diff --git a/README.md b/README.md index 13f5c77403f..1bb0a15a2d1 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,26 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![CI Status](https://github.com/AY2223S1-CS2103T-W12-2/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2223S1-CS2103T-W12-2/tp/actions) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
+* This is **a brownfield 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. + * Can be used by secretaries and treasurers to manage the club they belong to. + * Can also be used to take notes and as a bare-boned address book as well. +* The project simulates an ongoing software project for a desktop application (called _SectresBook_) 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. +* It is named `SectresBook` because it is used by Secretaries and Treasurers of clubs to manage their club information. +* For the detailed documentation of this project, see the **[SectresBook Product Website](https://ay2223s1-cs2103t-w12-2.github.io/tp/)**. +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). + +------------- + +## Acknowledgements + +The background of the program is a large-scale city, attributed to [GiulioDesign94](https://www.deviantart.com/giuliodesign94)'s [Big City](https://www.deviantart.com/giuliodesign94/art/Big-City-198672166). + +The icon of this program is an edited image from DepositPhotos, found [here](https://depositphotos.com/471137460/stock-illustration-book-yellow-glowing-neon-icon.html). + +The fonts used in this application are [Minion](https://fonts.adobe.com/fonts/minion) by Robert Slimbach and [Bender](https://www.1001fonts.com/bender-font.html) by Ivan Gladkikh, Oleg Zhuravlev and published by Jovanny Lemonad. + +This team recognises that the rights to the images go to their respective owners. diff --git a/build.gradle b/build.gradle index 108397716bd..8b388ebebb6 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,10 @@ test { finalizedBy jacocoTestReport } +run { + enableAssertions = true +} + task coverage(type: JacocoReport) { sourceDirectories.from files(sourceSets.main.allSource.srcDirs) classDirectories.from files(sourceSets.main.output) @@ -42,7 +46,7 @@ task coverage(type: JacocoReport) { dependencies { String jUnitVersion = '5.4.0' - String javaFxVersion = '11' + String javaFxVersion = '11.0.2' implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' @@ -66,7 +70,7 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'SectresBook.jar' } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..3fa019980f4 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,67 @@ title: About Us We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg). -You can reach us at the email `seer[at]comp.nus.edu.sg` +You can reach us on our GitHub portfolios, links provided below. ## Project team -### John Doe +### T Neethesh - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/Neethesh26)] +[[portfolio](team/neethesh26.md)] -* Role: Project Advisor +* Role: Developer +* Responsibilities: + * Feature (EditLoan, Loan History) + * English Proofreading + * Meeting coordinator -### Jane Doe +### Chen Ruihan - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/rui-han-crh)] +[[portfolio](team/rui-han-crh.md)] -* Role: Team Lead -* Responsibilities: UI +* Role: Developer +* Responsibilities: + * User Interface + * Feature (Loans) + * English Proofreading + * Project Embellishments -### Johnny Doe +### Ryan Chua - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/ryanczx)] +[[portfolio](team/ryanczx.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Tags -### Jean Doe +### Chee Zhong Wei - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/czhongwei)] +[[portfolio](team/czhongwei.md)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Find, Birthday, Documentation -### James Doe +### Jiang Pinran - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/Pinran-J)] +[[portfolio](team/pinran-j.md)] * Role: Developer -* Responsibilities: UI +* Responsibilities: + * Feature (All notes related) + * English Proofreading + * Team meeting management + diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 46eae8ee565..a73328fbb8a 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,14 +2,68 @@ layout: page title: Developer Guide --- -* Table of Contents -{:toc} + +## Table of Contents +* [Acknowledgements](#acknowledgements) +* [Overview](#overview) +* [Setting up, getting started](#setting-up-getting-started) +* [Design](#design) + * [Architecture](#architecture) + * [UI Component](#ui-component) + * [Logic Component](#logic-component) + * [Model Component](#model-component) + * [Storage Component](#storage-component) +* [Properties Objects](#properties-objects) + * [Properties of Person Objects](#properties-of-person-objects) + * [Properties of Note Objects](#properties-of-note-objects) +* [Implementation](#implementation) + * [Person Features](#person-features) + * [Edit Person](#edit-person) + * [Delete Person](#delete-person) + * [Find Person](#find-person) + * [Edit Loan](#edit-loan-value-and-history-of-person-by-editloan-feature) + * [Find by Tag](#find-persons-and-notes-by-tag) + * [Notes Features](#notes-features) + * [Add Note](#add-note) + * [Delete Note](#delete-note) + * [Edit Note](#edit-note) + * [UI Features](#ui-features) + * [General UI Design and Mechanism](#general-ui-design-and-mechanism) + * [Inspect Feature](#inspect-feature) + * [Showing and Hiding Panel Feature](#showing-and-hiding-the-notes-panel-feature) + * [[Proposed] Undo redo feature](#proposed-undoredo-feature) +* [Documentation, logging, testing, configuration, dev-ops](#documentation-logging-testing-configuration-dev-ops) +* [Appendix](#appendix-requirements) + * [Requirements](#appendix-requirements) + * [Product scope](#product-scope) + * [User stories](#user-stories) + * [Use Cases](#use-cases) + * [Non-Functional Requirements](#non-functional-requirements) + * [Glossary](#glossary) + * [Instructions for manual testing](#appendix-instructions-for-manual-testing) + -------------------------------------------------------------------------------------------------------------------- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +The background of the program is a large-scale city, attributed to [GiulioDesign94](https://www.deviantart.com/giuliodesign94)'s [Big City](https://www.deviantart.com/giuliodesign94/art/Big-City-198672166). + +The icon of this program is an edited image from DepositPhotos, found [here](https://depositphotos.com/471137460/stock-illustration-book-yellow-glowing-neon-icon.html). + +This team recognises the rights to the images go to their respective owners. + +-------------------------------------------------------------------------------------------------------------------- + +## **Overview** + +SectresBook helps secretaries to maintain all the information of the members of their club by collating a list of identifiable information, past loan records and future tasks. + +Our main target audience is secretaries, but this may be expanded to any person in need of loan tracking and task tracking features provided by our software. + +This Developer Guide is written for maintainers of SectresBook. + +Designers and developers who wish to extend or morph this product may also use this documentation to gain a better understanding of the design of this software. -------------------------------------------------------------------------------------------------------------------- @@ -18,6 +72,7 @@ title: Developer Guide Refer to the guide [_Setting up and getting started_](SettingUp.md). -------------------------------------------------------------------------------------------------------------------- +
## **Design** @@ -73,16 +128,32 @@ The **API** of this component is specified in [`Ui.java`](https://github.com/se- ![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`, `NoteListPanel`, `PersonInspectPanel` `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 is separate from the state of the `Model` and changes to the `UI` will not affect the `Model`, but changes to the `Model`'s state will affect the information displayed by the `UI` elements. + +Similarly, changes to the visual aspects of `UI`, such as current person viewed, or the filtered state of the lists, will not be saved to file as they do not affect the `Model`'s data. + 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 `Notes` object residing in the `Model`. + +This component may be controlled by commands through the `CommandResult` class, which specifies an optional `UiState` that the program should be in at the end of the command execution. This is parsed finally in the `MainWindow::executeCommand` which invokes necessary changes to the UI. + +For more information related to the implementation of the UI, please review the section [UI Features](#ui-features). + +#### UI Elements +![](images/UiLabeled.png) +1. **Command Box**: A text box that allows users to enter in commands for later execution. +2. **ResultDisplay**: A readonly text box that serves as a console to give feedback from the `Logic` component to the user, such as error messages or logs. +3. **Person List**: A horizontal sliding list that displays all persons in the SectresBook. This list can be filtered to display only relevant people according to some predicate. +4. **Note List**: A vertical sliding list that displays all notes in the SectresBook. This list can be filtered to display only relevant notes according to some predicate. +5. **Person Inspect Panel**: This Panel displays data of a person, using the UI command `inspect`. ### Logic component @@ -102,9 +173,13 @@ The Sequence Diagram below illustrates the interactions within the `Logic` compo ![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.
+Commands such as Edit and Delete feature the ability to delete by name, which utilises the Find feature. Illustrated here is how `execute("edit Lynette")` interacts with the Logic component, using a sequence diagram + +![Interactions Inside the Logic Component for the `edit Lynette` Command](images/EditSequenceDiagram.png) + Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command: @@ -121,17 +196,11 @@ 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 address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object) and all `Note` objects (contained in a `NoteBook` 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.
- - - -
- +
### Storage component @@ -149,14 +218,483 @@ The `Storage` component, Classes used by multiple components are in the `seedu.addressbook.commons` package. -------------------------------------------------------------------------------------------------------------------- +
+ +## Properties Objects + +### Properties of Person Objects + +This section explains the components of a person object and how they can be used to track information. + + + +A person object contains editable properties: +1. Name + - Describes the full name of the person +2. Phone + - Records the mobile phone contact number of the person +3. Email + - Records the email address of the person +4. Address + - Records the home address of the person +5. Birthday + - Records the birthday of the person +6. Tag + - Optionally tags the person with a variable number of tags for easy reference within the SectresBook. + +And a non-editable properties: +1. Loan + - Tracks the current loan amount of the person. A positive number means that the person currently owes money to the club, a negative number means that money is due to be paid to the person. +2. List of Loan Histories + - Tracks the transactional history of incoming and outgoing loans. Each Loan History comprises a Loan and a Record. + +During instantiation, a person object can be declared with all fields, but during editing, Loan must use a specialised command `editLoan` to transform its data. +
+ +### Properties of Note Objects + +This section explains the components of a note object and how they can be used to track information. + + -## **Implementation** +A note object contains editable properties: +1. Title + - The title of the note, which can be searched by. +2. Content + - The description of the note. +3. Tags + - Optional tags that can be assigned to the notes, after-which every person with that tag will be associated with the notes. + +During instantiation, a note object can be declared with any of these properties. + +------------------- + +## Implementation This section describes some noteworthy details on how certain features are implemented. +### Person Features + +#### Edit Person + +The Edit Person feature is facilitated by the `EditCommand` which utilises the `FindCommand`. It allows users to edit any editable field of a person given the index of the person, or the name of the person. + +If given a name that does not correspond to any person in the SectresBook, the edit features performs the same operations as the `Find` command. + +Given below is an example usage scenario and how the edit mechanism behaves at each step. + +Step 1. The user enters the edit command, with either the index or the person's name. + +Step 2a If an index is entered, the `EditCommandParser` carries this index to the `EditCommand`, which retrieves the `Person` to edit by getting the `Model`'s current `FilteredList` and retrieving by index. + +Step 2b. If a non-number is entered, the `EditCommandParser` invokes the `FindCommandParser#parse` method and executes it at the same time with `FindCommand#execute`. The `FilteredList` is then checked to ensure that there is exactly one person that corresponds with the search term. Otherwise, the method short-circuits with ambiguity errors (more than 1 person) or invalid person errors (no persons at all). If successful, `EditCommandParser` returns a new `EditCommand` with a one-based-index of 1. + +- Example of ambiguity error message: +> There is more than 1 person with the name [NAME] + +- Example of invalid name error message: +> There is nobody with the name [NAME] + +Step 3. `EditCommand#execute` is called by the `LogicManager`. The person to edit is retrieved by the index given and a new edited person is created by copying over non-transformed fields and replacing the transformed field. + +Step 4. The `editedPerson` is then set to replace the previous state of the `Person` object in the `Model` with `Model#setPerson`. + +The following sequence diagram shows how the `edit` feature works. + +![Interactions Inside the Logic Component for the `delete 1` Command](images/EditSequenceDiagram.png) + +#### Delete Person + +The delete person feature is facilitated by `DeleteCommand`. It allows users to delete a person from the SectresBook that match the full +First name or Last name of the person. + +Given below is the example usage scenario and how the delete feature behaves at each step. + +Step 1. The user executes 'delete David' command to delete a person with the name 'David' from the SectresBook. + +Step 2. The `DeleteCommandParser` creates a `FindCommand` with 'David'. + +Step 3. The `DeleteCommand` is executed with Index 1. + +The following sequence diagram shows how the delete command works: + + + +
: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.
+ +##### Design Considerations: + +**Aspect: How delete executes:** + +* **Alternative 1 (Currect choice):** Deletes person based on complete first/last name from input. + * Pros: Less strictness of input from the user to delete a person. + * Cons: User may accidentally delete a person not meant to be deleted. +* **Alternative 2:** Delete a person based on complete name i.e. first and last name required in input. + * Pros: Stricter input requirement, ensuring that persons are not accidentally deleted. + * Cons: Longer input required for the same output. + + +#### Find Person + +The Find Person feature is facilitated by 'FindCommand'. It allows users to find all Persons with names that are matching or phone number starting with any of the keywords. + +Given below is an example usage scenario and how the find feature behaves at each step. + +Step 1. The user executes 'find David' command to find all Persons in the address book that includes the name `David`. + +Step 2. A `FindCommand` is constructed with a `NameContainsKeywordPredicate` which checks through the list of persons in the address book and only shows those with their first/last name matching `David`. + +Step 3. The `FindCommand` is executed and the `NameContainsKeywordsPredicate` is used to update the filtered person list. + +The following sequence diagram shows how the find command works: + + + +
:information_source: **Note:** The lifeline for `FindCommandParser` and `FindCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. + +
+ +##### Design considerations: + +**Aspect: How find executes:** + +* **Alternative 1 (current choice):** Chooses person based on the whole first/last name matching the keyword. + * Pros: More specific Persons list after the find command. + * Cons: User needs to know the first/last name of the person they are trying to find. + +* **Alternative 2:** Chooses person if name contains the keyword. + * Pros: Easier to find person. + * Cons: Persons list may show other persons that are not desired by the user. + +#### Edit loan value and history of Person by EditLoan feature + +The Edit Loan feature is facilitated by the `EditLoanCommand` which utilises the `FindCommand`. It allows users to edit the loan value and update the loan history of a person given the index of the person, or the name of the person. + +If given a name that does not correspond to any person in the SectresBook, the edit feature performs the same operations as the `Find` command. + +Given below is an example usage scenario and how the editLoan mechanism behaves at each step. + +Step 1. The user enters the editLoan command, with either the index or the person's name. + +Step 2a If an index is entered, the `EditLoanCommandParser` carries this index to the `EditCommand`, which retrieves the `Person` to edit by getting the `Model`'s current `FilteredList` and retrieving by index. + +Step 2b. If a non-number is entered, the `EditLoanCommandParser` invokes the `FindCommandParser#parse` method and executes it at the same time with `FindCommand#execute`. The `FilteredList` is then checked to ensure that there is exactly one person that corresponds with the search term. Otherwise, the method short-circuits with ambiguity errors (more than 1 person) or invalid person errors (no persons at all). If successful, `EditLoanCommandParser` returns a new `EditLoanCommand` with a one-based-index of 1. + +- Example of ambiguity error message: +> There is more than 1 person with the name [NAME] + +- Example of invalid name error message: +> There is nobody with the name [NAME] + +Step 3. `EditLoanCommand#execute` is called by the `LogicManager`. The person to edit is retrieved by the index given and a new edited person is created by copying over non-transformed fields and replacing the transformed field. + +Step 4. The `editedPerson` is then set to replace the previous state of the `Person` object in the `Model` with `Model#setPerson`. + +The following sequence diagram shows how the `editLoan` feature works with index. + + +
+ +#### Find Persons and Notes by Tag + +The find Persons and Notes by Tag feature (called `findTag`) is facilitated by `FindTagCommand`. It allows users to find all Persons and Notes with the given Tags. + +Given below is an example usage scenario and how the findTag feature behaves at each step. + +Step 1. The user executes `findTag Finance` command to find all Persons and Notes in the SectresBook with the tag `Finance`. + +Step 2. A `FindTagCommand` is constructed with a `PersonTagsContainsKeywordsPredicate` and `NoteTagsContainsKeywordsPredicate` which will check through the list of Persons and Notes in the SectresBook and only show those with the tag `Finance`. + +Step 3. The `FindTagCommand` is executed and the `PersonTagsContainsKeywordsPredicate` and `NoteTagsContainsKeywordsPredicate` is passed to model to update the Person List and Note List to only show Persons and Notes with the tag `Finance`. + +The following sequence diagram shows how the findTag command works: + + + +
:information_source: **Note:** The lifeline for `FindTagCommandParser` and `FindTagCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. + +
+ +##### Design considerations: + +**Aspect: How findTag executes:** + +* **Alternative 1 (current choice):** Goes through all Persons and Notes to check for Tag. + * Pros: Easy to implement (Similar to current find command). + * Cons: May have performance issues in terms of having to do many more steps. + +* **Alternative 2:** Goto searched Tags and get the Persons and Notes that each Tag points to. + * Pros: Will use fewer steps (Go directly to the Tags rather than looking through all Persons and Notes). + * Cons: Implementation would be more complicated. + + +### Notes Features + +#### Add Note + +The addNote mechanism is facilitated by `AddNoteCommand`. It extends `Command` and overrides `Command#execute()` to implement the following operation: +- `AddNoteCommand#execute()` : adds the specified note with its associated title and content into the list of notes to be kept track of. + +Given below is an example usage scenario and how the addNote mechanism behaves at each step. + +Step 1. The user launches the application and wishes to keep track of a note with the following attributes : +1. Title : Club meeting +2. Content : 3rd October 9pm, brief everybody on upcoming events. + +Step 2. The user executes `addNote title/Meeting content/3rd October 9pm`, which calls `LogicManager#execute()`. Subsequently, `AddressBookParser#parseCommand()` is called +which will create a `AddNoteCommandParser` object and call `AddNoteCommandParser#parse()`. This method will take the user's input and make sense of it to create a `Note` object. + +Step 3. An `AddNoteCommand` will be created and `AddNoteCommand#execute()` will be called by `LogicManager#execute()`. + +Step 4. `AddNoteCommand#execute()` will call the following method from `Model` : +- `addNote(toAdd)` + +Step 5. `AddNoteCommand#execute()` will return a `CommandResult` object which will display the following message back to the user: +> New note added: Title: Meeting, Content: 3rd October 9pm + +The following sequence diagram shows how the addNote operation works: + +![AddNoteSequenceDiagram](images/AddNoteSequenceDiagram.png) + +##### Design considerations + +**Aspect: How Title and Content are represented:** + +* **Alternative 1 (current choice):** Title and Content as separate objects. + * Pros: Easy to validate Title/Content. (In the respective classes) + * Cons: May have performance issues in terms of memory usage(Many objects might be created). + +* **Alternative 2:** Title and Content as fields of Note + * Pros: Will use less memory (Fewer objects created). + * Cons: Harder to validate Title/Content. Better OOP(Object-oriented programming) design. + +#### Delete Note + +The deleteNote mechanism is facilitated by `DeleteNoteCommand`. It extends `Command` and overrides `Command#execute()` to implement the following operation: +- `DeleteNoteCommand#execute()` : deletes the note at the specified index from the note list. + +Given below is an example usage scenario and how the addNote mechanism behaves at each step. + +Step 1. The user launches the application and wishes to delete a note that no longer needs to be kept track of. The user lists the current notes: +1. Title: Meeting, Content: 3rd October 9pm +2. Title: Event, Content: Remind club members to attend. + +The user has decided to delete note 1. + +Step 2. The user executes `deleteNote 1`, which calls `LogicManager#execute()`. Subsequently, `AddressBookParser#parseCommand()` is called +which will create a `DeleteNoteCommandParser` object and call `DeleteNoteCommandParser#parse()`. This method will take the user's input and make sense of it to get the index of note to be deleted. + +Step 3. A `DeleteNoteCommand` object will be created and `DeleteNoteCommand#execute()` will be called by `LogicManager#execute()`. + +Step 4. `DeleteNoteCommand#execute()` will call the following method from `Model` : +- `getAddressBook()` +- `deleteNote(noteToDelete)` + +Step 5. `DeleteNoteCommand#execute()` will return a `CommandResult` object which will display the following message back to the user: +> Deleted Note: Title: Meeting, Content: 3rd October 9pm + +The following sequence diagram shows how the addNote operation works: + +![DeleteNoteSequenceDiagram](images/DeleteNoteSequenceDiagram.png) + +##### Design considerations + +**Aspect: How the note to be deleted is specified:** + +* **Alternative 1 (current choice):** Note is specified by index. + * Pros: Easy to implement. + * Cons: Would need to use listNotes command or gui to allow easy identification of index of note. + +* **Alternative 2:** Note is specified by Title. + * Pros: Would be more precise (Title of notes are unique). + * Cons: Long command would be needed to delete a note with a long Title. + +#### Edit Note + +The editNote mechanism is facilitated by `EditNoteCommand` and utilises the `FindNoteCommand`. It extends `Command` and overrides `Command#execute()` to implement the following operation: +- `EditNoteCommand#execute()` : edits the note at the specified index (or with matching title) from the note list. + +Given below is an example usage scenario and how the editNote mechanism behaves at each step. + +Step 1. The user launches the application and wishes to edit a note's title with a new title. The user lists the current notes: +1. Title: Meeting, Content: 3rd October 9pm +2. Title: Event, Content: Remind club members to attend. + +The user has decided to edit the first note titled `Meeting` with a new title, `Club Meeting`. + +Step 2a. If an index is entered (user executes `editNote 1 title/Club Meeting`), the `EditNoteCommandParser` carries this index to the `EditNoteCommand`, which retrieves the `Note` to edit by getting the `Model`'s current `FilteredList` and retrieving by index. + +Step 2b. If a non-number is entered (user executes `editNote meeting title/Club Meeting`), `EditNoteCommandParser` calls `FindNoteCommandParser#parse()` and executes `FindNoteCommand#execute()` concurrently. + +`FilteredList` is then checked to ensure that exactly one note corresponds with the search term. + +Otherwise, the method short-circuits with ambiguity errors (more than 1 note) or invalid note errors (no notes at all). If successful, `EditNoteCommandParser` returns a new `EditNoteCommand` object. + + +- Example of ambiguity error message: +> There is more than 1 note with meeting in their title! +> +> Please use a more unique specifier or use indices to edit. + +- Example of invalid title error message: +> There are no notes with meeting in their titles in the list! + +Step 3. `EditNoteCommand#execute()` will be called by `LogicManager#execute()`. The note to edit is retrieved by the index given and a new edited note is created by copying over non-transformed fields and replacing the transformed field. + +Step 4. The `editedNote` is then set to replace the previous state of the `Note` object in the `Model` with `Model#setNote`. + +The following sequence diagram shows how the `editNote` feature works. + +![EditNoteSequenceDiagram](images/EditNoteSequenceDiagram.png) + +The following activity diagram shows the workflow of `editNote` feature. + +![EditNoteActivityDiagram](images/EditNoteActivityDiagram.png) + +##### Design considerations + +**Aspect: How the note to be edited is specified:** + +* **Alternative 1 (current choice):** Note is specified by index or title. + * Pros: Better convenience for users. + * Cons: Would need to use findNote command for notes specified by title. (more dependencies) + +* **Alternative 2:** Note is specified by Title. + * Pros: Easy to implement + * Cons: Not flexible for users. + + +### UI Features + +#### General UI Design and Mechanism + +During the creation of the new UI, a lot of the FXML structure and the relationships between containers of the UI had to be refactored. + +![](images/UIComponentsLabeled.png) + +The UI is divided into 2 major sections - one occupied by the `CommandBox` and another occupied by the `WindowAnchorPane`. + +The `WindowAnchorPane` consists of an StackPane. +- The `ResultDisplay` is in the first layer +- The `PersonListPanel`, `InspectionPanel` and the `NotesListPanel` are in the second layer below. + +The `ResultDisplay` is _click-through_ (does not capture any mouse clicks) and normally has an opacity of 0, so it is effectively hidden. + +The `InspectionPanel` is another anchor pane divided into two left and right elements: + - A basic information `HBox` on the left + - A loan history list view display on the right + +These two elements in the `InspectionPanel` will always maintain the same ratio, basic information to loan history list view, of 2:3. + +When the user clicks the `CommandBox` or presses the `SPACE` key, this triggers an event on the `CommandBox` that invokes a transition to the `ResultDisplay` to show the display. As it is on the first layer, the `ResultDisplay` will partially cover the elements below it. + +Note that the `ResultsDisplay` never reach full opacity, instead an opacity of 0.8 allows the elements below to be partially visible. + +Here are the anchor points of the three major panes within the second layer of `WindowAnchorPane`: + +- The `PersonListPanel` has a left anchor of `0`, top anchor of `0`, right anchor of `0.6` with respect to window width (starting from left) and bottom anchor of `0.45` with respect to window height (starting from top). +- The `InspectionPanel` has a left anchor of `0`, top anchor of `0.45`, right anchor of `0.6` with respect to window width (starting from left) and bottom anchor of `0` with respect to window height (starting from top). +- The `NotesListPanel` has a left anchor of `0.6`, top anchor of `0`, right anchor of `0` with respect to window width (starting from left) and bottom anchor of `0` with respect to window height (starting from top). + +The arbitrary values above are actually boundaries shared by the three panels and may be manipulated to change the view of the three major elements together. We may imagine the `PersonListPanel` to be glued to the `NotesListPanel` on its right and the `InspectionPanel` below, likewise for the other two elements. This allows the ratios to be adjusted together by simply change the vertical anchor or horizontal anchors. + +The Observer Pattern is prevalent throughout the UI design in order to update other components in response to any change to the UI. One such example is the [`inspect` command](#inspect-feature). + +This activity diagram details how interactions with the UI is controlled + +![](images/UIActivityDiagram.png) + +##### Design Considerations + +As the main window of the application is resizable, the ratio of the anchors maintains the aspect of each panel with respect to the window size (unlike constant values which will not change according to window size). + +The resizing is handled through the _Observer Pattern_, thankfully existing by default in JavaFX, which updates the proportions of the components upon any change to the height or width of the window. + +The main problem came from estimating the correct current space allocated by the window size. + +As the three main components are within the `WindowAnchorPane`, and the `WindowAnchorPane` shares its space with an unnamed `StackPane` described by the structure above, this meant that the `WindowAnchorPane` has less space than the actual window size. To make things worse, JavaFX overestimates the actual scene proportions in relation to the window size. + +A padding of around 200px was used to help `WindowAnchorPane` displace it's top height to the true position it is at and a bottom padding of 20px was added to give some room from the bottom of the window. The height and width passed into function resizing the anchor panes must always be adjusted according the scene size and the offsets. + +A visual defect exists when the screen size exceeds 1080p, as the Inspection Panel is no longer able to stay attached to the anchor point at the bottom of the screen. This defect worsens as the window gets taller. + +#### Inspect Feature + +The inspect command is a UI-Centric command that controls which person's details are currently shown in the inspection panel. + +This is functionally similar to clicking on a person's card, which also updates the information in the inspection panel. + +This features uses an Observer on the `SelectionModel` of the `ListView` of persons, updating the `InspectionPanel` whenever a new person is selected. + +Step 1. The user executes `'inspect David'` command to view the details of David. + +Step 2. An `InspectCommandParser` is parsed with `David`. + +Step 3. The `InspectCommand` is created containing a keyword `David`. + +Step 4. The `InspectCommand` is executed and a `CommandResult` is created with the `UiState` `inspect` and arguments `David`. + +Step 5. The `MainWindow` is receives the `CommandResult` and reads the `UiState` part of the result. This directs it to update the inspect panel. + +Step 6. The Person List is retrieved from the `MainWindow` and its selection model is accessed. From here, if the given argument is a number, we will index the person through the order in the list. Otherwise, we will search through the entire list until first person matching the keywords is returned. + +Step 7. The Inspection Panel is retrieved from the main window and has its properties updated from the person's information that was returned. + +The following sequence diagram shows how the inspect command works: + + + +
:information_source: **Note:** The lifeline for `InspectCommandSequence` and `InspectCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. + +
+ +##### Design considerations: + +**Aspect: How to design a UI command** + +Unlike other command, this command does not mutate any underlying data. One challenge is that the execution flow is different from other commands which mutate data, making the implementation less direct. + +We had to find a good insertion point where the information carried from the user input could be used in the program without the need for major refactoring. We realised that the user's input is first taken in through the `MainWindow` class, and through following the function calls, would be used in `MainWindow::executeCommand`. So the inspection operation is effectively added at the end of the executeCommand just before it terminates. + +Inspiration was taken from the execution flow of `handleExit` and `handleHelp` prior to the construction of the new UI. + +The procedures for `handleExit` and `handleHelp` were changed by refactoring `CommandResult` to carry an ideal state that the UI is expected to be in by the end of the execution. A switch statement was added to the bottom of the `executeCommand` function, much like how the normal commands are parsed, to deal with UI-Centric commands like `help`, `exit` and `inspect`. + +#### Showing and Hiding the Notes Panel Feature + +Here are the anchor points of the three major panes within the `WindowAnchorPane`: + +- The `PersonListPanel` has a left anchor of `0`, top anchor of `0`, right anchor of `0.6` with respect to window width (starting from left) and bottom anchor of `0.45` with respect to window height (starting from top). +- The `InspectionPanel` has a left anchor of `0`, top anchor of `0.45`, right anchor of `0.6` with respect to window width (starting from left) and bottom anchor of `0` with respect to window height (starting from top). +- The `NotesListPanel` has a left anchor of `0.6`, top anchor of `0`, right anchor of `0` with respect to window width (starting from left) and bottom anchor of `0` with respect to window height (starting from top). + +Notice that the `PersonListPanel` and `InspectionPanel` share a boundary of the ratio `0.45` with respect to the window height and the both of them share a boundary of `0.6` with respect to screen width with the `NotesListPanel`. + +The command `hideNotes` is effectively accomplished by interpolating the ratios smoothly over time to create a sliding effect. To maintain the aspect ratio of the notes panel and prevent deformities, the right anchor interpolates from 0 to `0.6 - 1 = -0.4`, as the left anchor interpolates from `0.6` to `1`. + +This maintains the constant size of `x0.4` with respect to window size during the transition. + +A fading transition is applied across the same time to smoothly reduce the opacity of the panel. + +The time interval set for this transition is `0.3` seconds. + +The `showNotes` implementation is exactly the inverse of the `hideNotes` implementation across time. + + +##### Design considerations: + +**Aspect: Challenges related to resizing** + +Because hiding the notes panel will also pull the inspection panel longer, and that the inspection panel is divided into two parts itself, the `InspectionPanel` is also further another anchor pane that maintains the ratio of the width between the basic information display and the loans history property. + +This concept was only implemented after the implementation of hideNotes, where visual inconsistencoes will start appearing due to disobeying the original ratio. + +The padding of the `InspectionPanel` also causes an issue if the right anchor of the pane is not manipulated with the left anchor, as the padding will snap the left anchor to the right of the right anchor. This causes the `InspectionPanel` to be pulled through the entire width of the `NotesListPanel` cause the entire `WindowAnchorPane` to overflow its allocated width. The consideration to slide the right anchor at a constant difference with respect to the left anchor was introduced to combat this issue. + ### \[Proposed\] Undo/redo feature -#### Proposed Implementation +##### Proposed Implementation The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: @@ -176,7 +714,7 @@ Step 2. The user executes `delete 5` command to delete the 5th person in the add ![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`. +Step 3. The user executes `add name/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`. ![UndoRedoState2](images/UndoRedoState2.png) @@ -211,7 +749,7 @@ Step 5. The user then decides to execute the command `list`. Commands that do no ![UndoRedoState4](images/UndoRedoState4.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. +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 name/David …​` command. This is the behavior that most modern desktop applications follow. ![UndoRedoState5](images/UndoRedoState5.png) @@ -219,26 +757,21 @@ The following activity diagram summarizes what happens when a user executes a ne -#### Design considerations: +##### Design considerations: **Aspect: How undo & redo executes:** * **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. + * Pros: Easy to implement. + * Cons: May have performance issues in terms of memory usage. * **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. + * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). + * Cons: We must ensure that the implementation of each individual command are correct. _{more aspects and alternatives to be added}_ -### \[Proposed\] Data archiving - -_{Explain here how the data archiving feature will be implemented}_ - - -------------------------------------------------------------------------------------------------------------------- ## **Documentation, logging, testing, configuration, dev-ops** @@ -257,42 +790,101 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: +* is acting as a secretary or a treasurer of a club with a lot of people +* has a need for a convenient way to organise paperwork and general information about the club * has a need to manage a significant number of contacts +* has a requirement to keep notes and tabs on people and projects * prefer desktop apps over other types * can type fast * prefers typing to mouse interactions * is reasonably comfortable using CLI apps -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: manage club members and track notes faster than a typical mouse/GUI driven app or by pen and paper ### 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…​ | +|----------|----------------------------------|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------| +| `* * *` | secretary | add club members’ information into the address book | keep track of their contact information. | +| `* * *` | secretary | edit a club member’s information | stay updated with them if their contact information changes. | +| `* * *` | secretary | delete a club member’s information from the address book | stop keeping track of them when they leave the club. | +| `* * *` | user | search for a person by their name or contact number | locate details of persons without having to go through the entire list. | +| `* *` | secretary | search contacts according to a specific tag | easily contact people in a whole group. | +| `*` | user | maintain a set of tasks to be done | keep track of things to be done. | +| `* * *` | treasurer | see the amount each person owes me | keep track of my finances. | +| `*` | person that keeps track of tasks | see who in my contacts lies under which tasks and which tasks lies under each contact | keep track of my deadlines. | +| `*` | treasurer | keep track of my club's fund and the budget assigned to every project | better organise and plan the club's finances. | +| `*` | secretary | archive the contact information of a club member that has left the club | contact old club members by looking at archived contacts. | +| `* *` | expert user | quickly use the CLI commands to speed up operations | enhance the productivity of meetings and task recording. | +| `* *` | secretary | pinpoint all the addresses of members and find the nearest amongst all members | organise club activities and events that minimise travel time. | +| `* * *` | treasurer | identify which member owes the club money | | +| `* *` | secretary | password protect the software | protect member's personal data. | +| `* *` | busy treasurer | get reminded of upcoming payments | ask for payments before they are due. | +| `*` | user | use the GUI elements to autofill the CLI | recognise and learn the commands for future typing. | +| `* *` | user | configure the GUI elements' size and colours | customise the application to my needs. | +| `* *` | secretary | set priorities on tasks | focus on tasks that are more important first. | +| `*` | secretary | get statistics on the number of tasks done | ensure that the club is on track with finishing tasks. | +| `*` | new user | learn how to use the commands using in-app guidance | easily pick up the commands and perform my duties | +| `* *` | new user | have sample data that I can test out commands with | familiarize myself with how the application works. | +| `* * *` | outgoing secretary | transfer the data to an incoming secretary | hand over my job without hassle. | -*{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 the `SectresBook` and the **Actor** is the `user`, unless specified otherwise) + +#### Use case: UC1 - Add a person + +**MSS** +1. User requests to add a person. +2. SectresBook adds the person to the list of persons. + + Use case ends. + +**Extensions** +* 1a. The given person already exists. + * 1a1. SectresBook shows an error message. + + Use case ends. +* 1b. Necessary fields are incomplete/empty. + * 1b1. Sectresbook shows an error message. + + Use case ends. + +#### Use case: UC2 - Update a person + +**MSS** +1. User requests to list persons. +2. SectresBook shows a list of persons. +3. User requests to update a specific person in the list. +4. SectresBook updates information of the person. + + Use case ends. + +**Extensions** +* 2a. The list is empty. + + Use case ends. +* 3a. The given index is invalid. + * 3a1. SectresBook shows an error message. + + Use case resumes at step 2. +* 3b. The command line arguments are invalid. + * 3b1. SectresBook shows an error message. + + Use case resumes at step 2. -**Use case: Delete a person** +#### Use case: UC3 - Delete a person **MSS** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User requests to list persons. +2. SectresBook shows a list of persons. +3. User requests to delete a specific person in the list. +4. SectresBook deletes the person. Use case ends. @@ -304,24 +896,197 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli * 3a. The given index is invalid. - * 3a1. AddressBook shows an error message. + * 3a1. SectresBook shows an error message. Use case resumes at step 2. -*{More to be added}* +#### Use case: UC4 - Find a person -### Non-Functional Requirements +**MSS** +1. User request to find using keyword. +2. SectresBook shows a list of persons matching keyword. + + Use case ends. + +#### Use case: UC5 - Display list of persons + +**MSS** +1. User requests to list persons. +2. SectresBook displays the list of persons stored. -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. + Use case ends. -*{More to be added}* +#### Use case: UC6 - Edit Loan of person + +**MSS** +1. User requests to list persons. +2. SectresBook shows a list of persons. +3. User requests to update the loan of a specific person in the list. +4. SectresBook updates loan and loan history of the person. + +**Extensions** +* 2a. The list is empty. + + Use case ends. +* 3a. The given index is invalid. + * 3a1. SectresBook shows an error message. + + Use case resumes at step 2. +* 3b. The command line arguments are invalid. + * 3b1. SectresBook shows an error message. + + Use case resumes at step 2. + + +#### Use case: UC7 - Find Person by their tag + +**MSS** +1. User requests to find using a tag keyword. +2. SectresBook shows a list of persons matching the tag keyword. + + Use case ends. + +#### Use case: UC8 - Add a Note + +**MSS** +1. User requests to add a note. +2. SectresBook adds the note to the list of notes. + + Use case ends. + +**Extensions** +* 1a. The given note title already exists. + * 1a1. SectresBook shows an error message. + + Use case ends. +* 1b. Necessary fields are incomplete/empty. + * 1b1. Sectresbook shows an error message. + + Use case ends. + +#### Use case: UC9 - Update a Note + +**MSS** +1. User requests to list notes. +2. SectresBook shows a list of notes. +3. User requests to update a specific note in the list. +4. SectresBook updates information of the note. + + Use case ends. + +**Extensions** +* 2a. The list is empty. + + Use case ends. +* 3a. The given index is invalid. + * 3a1. SectresBook shows an error message. + + Use case resumes at step 2. +* 3b. The command line arguments are invalid. + * 3b1. SectresBook shows an error message. + + Use case resumes at step 2. + +#### Use case: UC10 - Delete a Note + +**MSS** +1. User requests to list notes. +2. SectresBook shows a list of notes. +3. User requests to delete a specific note in the list. +4. SectresBook deletes the person. + + Use case ends. + +**Extensions** + +* 2a. The list is empty. + + Use case ends. + +* 3a. The given index is invalid. + + * 3a1. SectresBook shows an error message. + + Use case resumes at step 2. + +#### Use case: UC11 - Find a Note + +**MSS** +1. User request to find using keyword. +2. SectresBook shows a list of notes matching keyword. + + Use case ends. + +#### Use case: UC12 - Display list of notes + +**MSS** +1. User has completed [UC11](#use-case-uc11---display-list-of-notes) +2. User requests to list notes. +3. SectresBook displays the list of all notes stored. + + Use case ends. + +#### Use case: UC13 - Hide Note section + +**MSS** +1. User requests to hide the notes section of the Sectresbook. +2. Sectresbook hides the note section, extending the addressbook section. + + Use case ends. + +**Extensions** +* 1a. Already hiding the notes section. + + Use case ends. + +#### Use case: UC14 - Show note section + +**MSS** +1. User requests to show the notes section. +2. Sectresbook shows the notes section on the right side of the interface. + + Use case ends. + +**Extensions** + +* 1a. Already showing the notes section. + + Use case ends. + +#### Use case: UC15 - Exit program + +**MSS** +1. User requests to exit program. +2. Sectresbook closes the program. + + Use case ends. + +### Non-Functional Requirements + +1. Should run independently of remote servers. +2. Should not use a relational database management system to store data. +3. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. +4. Should not require internet connection, all operations are performed locally. +5. Should not consume a lot of battery to keep it running in the background. +6. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +7. Should be able to hold up to 1000 notes without a noticeable sluggishness in performance for typical usage. +8. Should be able to respond to any commands within 2 seconds as long as there are under 1000 entries stored in the application. +9. 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. +10. Should automatically save any changes to the data in the storage directly. +11. The product is intended only for a single user (i.e. not a multi-user product) ### Glossary * **Mainstream OS**: Windows, Linux, Unix, OS-X +* **Java 11**: Eleventh version of the Java Platform, Standard Edition Development Kit (JDK). SectresBook requires this to be installed to run. * **Private contact detail**: A contact detail that is not meant to be shared with others +* **Note**: A segment of text that describes a task to be done, coupled with tags that reference people in the SectresBook who are associated with the given task. +* **Secretary**: A person acting as overseers for the administrative functions of a club. +* **Treasurer**: A club member that manages and accounts for all the funds of a club. +* **Tag**: A label that groups related people or notes together, such that they can be referred to as a single encapsulated entity specified by the tag. +* **Graphical User Interface (GUI)**: An image-based interface that is more visually appealing than a command-line interface and encapsulates information through the use of icons and images. +* **Command Line Interface (CLI)**: A text-based interface that receives typed commands as input and returns textual feedback as output. + -------------------------------------------------------------------------------------------------------------------- @@ -338,40 +1103,249 @@ testers are expected to do more *exploratory* testing. 1. Initial launch - 1. Download the jar file and copy into an empty folder + 1. Download the 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. + 1. 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 - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. + 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.
+ 1. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ - ### Deleting a person 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. - - 1. 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)
- Expected: Similar to previous. - -1. _{ more test cases …​ }_ - -### Saving data - -1. Dealing with missing/corrupted data files - - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ - -1. _{ more test cases …​ }_ + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 2. Test case: `delete 1`
+ Expected: First contact is deleted from the list. Result display shows details of the deleted contact. + + 3. Test case: `delete 0`
+ Expected: No person is deleted. Result display shows error message stating non-positive indices not allowed. + + 4. Test case: `delete Charlotte`, assuming there is only 1 person with the name `Charlotte` in the Sectresbook.
+ Expected: Person with name `Charlotte` is deleted from the list. Result display shows details of the deleted contact. + + 5. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. + +2. Deleting a person, but not all persons are shown + + 1. Prerequisites: List a subset of persons using the `find` command. At least 1 person in the list. + + 2. Test case: `delete 1`, assuming there is more than or equal to 1 person in the Sectresbook.
+ Expected: First contact is deleted from the list. Result display shows details of the deleted contact. + + 3. Test case: `delete -1`
+ Expected: No person is deleted from the list. Result display shows error message stating non-positive indices not allowed. + + 4. Test case: `delete Charlotte`, assuming there is only 1 person with the name `Charlotte` in the Sectresbook.
+ Expected: Person with name `Charlotte` is deleted from the list. Result display shows details of the deleted contact shown. + + 5. Test case: `delete Sean`, assuming there is more than 1 person with the name `Sean` in the Sectresbook.
+ Expected: No person is deleted from the list. Sectresbook lists the contacts with `Sean` in their name. Result display shows error message stating more than one person with that name. + + 6. Other incorrect delete commands to try: `delete`, `delete phone/PHONE_NUMBER`, `...` (where `PHONE_NUMBER` is a phone number of a person).
+ Expected: No person is deleted from the list. Result display shows error message stating invalid command format. + +### Editing a person + +1. Editing a person after listing all persons or after finding a list of persons (list should contain more than 0 people). + + 1. Test case: `edit 1 phone/98112233`, assuming there is more than or equal to 1 person in the Sectresbook.
+ Expected: Phone number of the first contact in the list is edited. Result display shows details of the edited contact. + + 2. Test case: `edit 5 tag/Friend`, assuming there is less than 5 people currently displayed in the Sectresbook.
+ Expected: No person is edited. Result display shows error message stating invalid index. + + 3. Test case: `edit Charlotte tag/Friend`, assuming there is only 1 person with the name `Charlotte` in the Sectresbook.
+ Expected: The tag of the Person with the name `Charlotte` is edited. Result display shows details of the edited contact. + + 4. Test case: `edit Sean tag/Colleague`, assuming there is more than 1 person with the name `Sean` in the Sectresbook.
+ Expected: No person is edited from the list. Sectresbook shows the list of persons with `Sean` in their name. Result display shows error message stating more than one person with that name. + + 5. Other incorrect edit commands to try: `edit`, `edit 2 phone/TEXT`, `...` (where `TEXT` is an input with only alphabets).
+ Expected: No person is edited in the list. Result display shows error message stating phone number should be only contain numbers and at least 3 digits. + +### Editing a loan of a person + +1. Editing a loan of a person after listing all persons or after finding a list of persons (list should contain more than 0 people). + + 1. Test case: `editLoan 1 amt/10 reason/Logistics`, assuming there is more than or equal to 1 person in the Sectresbook.
+ Expected: Loan amount and history of the first contact is updated. Result display shows details of the edited contact. + + 2. Test case: `editLoan 5 amt/100 reason/Logistics`, assuming there is less than 5 people currently displayed in the Sectresbook.
+ Expected: No person is edited. Result display shows error message stating invalid index provided. + + 3. Test case: `editLoan Charlotte amt/10 reason/Test`, assuming there is only 1 person with the name `Charlotte` in the Sectresbook.
+ Expected: Loan amount and history of the Person with the name `Charlotte` is edited. Result display shows details of the edited contact. + + 4. Test case: `editLoan Sean amt/10 reason/Test`, assuming there is more than 1 person with the name `Sean` in the Sectresbook.
+ Expected: No person is edited from the list. Sectresbook shows the list of persons with `Sean` in their name. Result display shows error message stating more than one person with that name. + + 5. Other incorrect editLoan commands to try: `editLoan`, `editLoan amt/`,`editLoan 1 amt/TEXT reason/TEXT`, `...` (where `TEXT` is an input with only alphabets).
+ Expected: No person is edited in the list. Result display shows error message stating invalid command format. + +### Finding a person + +1. Finding a person with the given keyword. + + 1. Test case: `find Ryan`, assuming there is at least 1 person with the substring `Ryan` in the Sectresbook.
+ Expected: Person list is updated with contacts that contain the substring `Ryan`. Result display shows the number of persons listed. + + 2. Test case: `find Jack`, assuming there is no one with the substring `Jack`.
+ Expected: Person list is updated, showing nobody in the list. Result display shows the number of persons listed. + + 3. Test case: `find 8445`, assuming there is at least 1 person with phone number starting with `8445` in the Sectresbook.
+ Expected: Person list is updated with contacts with phone number starting with `8445`. Result display shows the number of persons listed. + + 4. Test case: `find 8445 Ryan`, assuming there is at least 1 person with phone number starting with `8445` and substring `Ryan` in their name in the Sectresbook.
+ Expected: Person list is updated with contacts that contain the substring `Ryan` and phone number starting with `8445`. Result display shows the number of persons listed. + + 5. Incorrect editLoan command to try: `editLoan`.
+ Expected: No change in the list shown. Result display shows message stating invalid command format. + +### Listing a person + +1. Listing all persons in the Sectresbook. + + 1. Test case: `list`
+ Expected: Person list is updated to show all persons in the Sectresbook. Result display shows success message. + + 2. Test case: `list TEXT`, where `TEXT` is any string input.
+ Expected: Person list is updated to show all persons in the Sectresbook. Result display shows success message. + +### Inspecting a person + +1. Inspecting a person while all persons are shown. + + 1. Test case: `inspect 1`, assuming there is at least 1 person shown in the person list.
+ Expected: Displays the information of the contact with index 1 in the person list. Result display shows success message. + + 2. Test case: `inspect Charlotte`, assuming there is only 1 person with the name `Charlotte` in the Sectresbook.
+ Expected: Displays the information of the contact with name `Charlotte` in the person list. Result display shows success message. + + 3. Test case: `inspect -1`
+ Expected: No change in the person being inspected. Result display shows error message stating inspection failed, name should be alphanumeric characters only. + + 4. Test case: `inspect Jack`, assuming there is more than 1 person with the name `Jack` in the person list.
+ Expected: No change in the person currently being inspected. Result display shows error message stating more than one person with that name. + + 5. Other incorrect inspect commands to try: `inspect`.
+ Expected: No change in the person currently being inspected. Result display shows error message stating nothing to inspect. + + +### Adding a note + +1. Adding a note with title that is yet to exist + 1. Test case: `addNote title/event content/november 3rd 4pm`
+ Expected: Note with title `event` and content `november 3rd 4pm` added into notes list. Result display shows message stating new note is added. Notes display panel shows new note. + + 2. Test case: `addNote title/event content/november 3rd 4pm tag/progs`
+ Expected: Result display shows message stating new note is added. Notes display panel shows new note. + + 3. Test case: incorrect `addNote` commands (e.g. `addNote`, `addNote title/event`, `addNote content/groceries`)
+ Expected: No new note is added to notes list. Result display shows error message stating invalid command format. + +2. Adding a note with a duplicate title + 1. Test case: `addNote title/event content/november 3rd 4pm`
+ Expected: No new note is added to notes list. Result display shows message stating that note already exists in the notes list. No change to the notes display panel. + +### Editing a note + +1. Editing a note + 1. Test case: `editNote 1 content/meeting at mpsh`, assuming there exists at least 1 note in the current notes list.
+ Expected: Note at index 1 in the current notes list have its content changed to `meeting at mpsh`. Result display shows message stating note was edited. + + 2. Test case: `editNote progs content/meeting at mpsh`, assuming there exists a note with title `progs`.
+ Expected: Note with title `progs` in the current notes list have its content changed to `meeting at mpsh`. Result display shows message stating note was edited. + + 3. Test case: `editNote 5 content/meeting at mpsh`, assuming there are less than 5 notes in the current notes list.
+ Expected: No note was edited. Result display shows error message stating index provided to be invalid. + + 4. Test case: `editNote content/meeting at mpsh`
+ Expected: No note was edited. Status bar display error message stating at least one field to be edited has to be provided. + + 5. Test case: incorrect `editNote` commands (e.g. `editNote`, `editNote content/abc`)
+ Expected: No note was edited. Result display shows error message stating invalid command format. + +### Deleting a note + +1. Deleting a note + 1. Test case: `deleteNote 1`, assuming there exists at least 1 note in the current notes list.
+ Expected: Note at index 1 is removed from the current notes list. Result display shows message stating note was deleted. + + 2. Test case: `deleteNote`
+ Expected: No note was deleted. Result display shows error message stating invalid command format. + +### Finding a note + +1. Finding a note + 1. Test case: `findNote progs`
+ Expected: Notes with title containing the keyword `progs` are shown on the notes display panel. Result display shows message stating number of notes listed. + If there are no notes with `progs` in the title, notes display panel will be empty. + + 2. Test case: `findNote`
+ Expected: No change to the notes display panel. Status bar display error message stating invalid command format. + +### List all notes + +1. List notes + 1. Test case: `listNote`
+ Expected: Notes display panel is populated with all existing notes. Result display shows message stating listed all notes. + +### Hide notes + +1. Hide notes + 1. Test case: `hideNotes`, assuming notes display panel is on shown on the GUI.
+ Expected: Notes display panel is hidden from the GUI. Persons list and Inspect section is extended horizontally to fill the GUI. Result display shows message stating notes panel hidden. + + 2. Test case: `hideNotes`, assuming notes display panel is already hidden. Result display shows message stating notes panel hidden.
+ Expected: No change to the GUI. Result display shows message stating notes panel hidden. + + 3. Other `hideNotes` commands with additional arguments (e.g. `hideNotes 1`, `hideNotes abc`)
+ Expected: Additional arguments are ignored, behaviour is same as `hideNotes`. + +### Show notes + +1. Show notes + 1. Test case: `showNotes`, assuming notes display panel is hidden from GUI.
+ Expected: Notes display panel appears on the right side of the GUI. Persons list and Inspect section becomes horizontally narrower. Result display shows message stating notes panel shown. + + 2. Test case: `showNotes`, assuming notes display panel is already shown on the GUI.
+ Expected: No change to the GUI. Result display shows message stating notes panel shown. + + 3. Other `showNotes` commands with additional arguments (e.g. `showNotes 1`, `showNotes abc`)
+ Expected: Additional arguments are ignored, behaviour is same as `showNotes`. + +### Finding person/notes based on tag + +1. Find using tag + 1. Test case: `findTag cs2103`
+ Expected: Persons list display persons tagged with `cs2103`. Notes display panel displays notes tagged with `cs2103`. Result display shows message stating number of people listed in the persons list.
+ If there is no person tagged with `cs2103`, the persons list will be empty.
+ If there is no note tagged with `cs2103`, the notes display panel will be empty. + +### Viewing Help + +1. Viewing help bar. + + 1. Test case: `help`
+ Expected: The help popup screen is shown. + +### Clearing all data + +1. Clearing all data + + 1. Test case: `clear`
+ Expected: Clears all data from the Sectresbook. + +### Exiting program + +1. Exiting the Sectresbook + + 1. Test case: `exit`
+ Expected: The program closes. diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 275445bd551..1de01ecf4d1 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -45,7 +45,7 @@ If you plan to use Intellij IDEA (highly recommended): 1. **Learn the design** - When you are ready to start coding, we recommend that you get some sense of the overall design by reading about [AddressBook’s architecture](DeveloperGuide.md#architecture). + When you are ready to start coding, we recommend that you get some sense of the overall design by reading about [SectresBook’s architecture](DeveloperGuide.md#architecture). 1. **Do the tutorials** These tutorials will help you get acquainted with the codebase. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 3716f3ca8a4..cf57b6f6e2f 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,38 +3,469 @@ layout: page title: User Guide --- -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. +# SectresBook -* Table of Contents -{:toc} +

+ +

--------------------------------------------------------------------------------------------------------------------- +SectresBook helps secretaries to **maintain all the information of the members of their club** by collating a list of identifiable information, past records, loan amounts and future tasks. This all-in-one tool eliminates the trouble of having to search through multiple notebooks or apps to find information regarding a club member, saving you time and effort so that you can focus on other tasks at hand. + +With speed and efficiency, you can interact with SectresBook using the **Command Line Interface (CLI)**[3](#glossary), while still having the benefits of a visually appealing **Graphical User Interface (GUI)**[4](#glossary). + +The intended audience of this User Guide are secretaries who are planning to use SectresBook, or secretaries who are already using SectresBook. + +This User Guide is an in-depth guide to help you start managing your contacts, notes and finances. It includes **installation instructions, features and how to use them, and [Frequently Asked Questions (FAQ)](#faq) for troubleshooting**, ensuring a smooth pickup of SectresBook. + +--- + +## Table of Contents + * [Introduction to SectresBook](#introduction-to-sectresbook) + * [Using this guide](#using-this-guide) + * [Quick start](#quick-start) + * [User Interface](#user-interface) + * [Properties](#properties) + + [Person Properties](#person-properties) + + [Notes Properties](#notes-properties) + + [Tag Properties](#tag-properties) + * [Features](#features) + + [Person Features](#person-features) + - [Adding a new person : `add`](#adding-a-new-person--add) + - [Editing a club member : `edit`](#editing-a-club-member--edit) + - [Editing loan of a person : `editLoan`](#editing-loan-of-a-person--editloan) + - [Deleting a person : `delete`](#deleting-a-person--delete) + - [Locating persons by name or contact number : `find`](#locating-persons-by-name-or-contact-number--find) + - [Listing all persons : `list`](#listing-all-persons--list) + - [Sorting by property : `sort` `[coming in v2.0]`](#sorting-by-property--sort-coming-in-v20) + + [Note Features](#note-features) + - [Adding Notes : `addNote`](#adding-notes--addnote) + - [Editing Notes : `editNote`](#editing-notes--editnote) + - [Deleting Notes : `deleteNote`](#deleting-notes--deletenote) + - [Locating a note by title : `findNote`](#locating-a-note-by-title--findnote) + - [Listing Notes : `listNote`](#listing-notes--listnote) + - [Sorting Notes : `sortNotes` `[coming in v2.0]`](#sorting-notes--sortnotes-coming-in-v20) + - [Hiding notes panel : `hideNotes`](#hiding-notes-panel--hidenotes) + - [Showing notes panel : `showNotes`](#showing-notes-panel--shownotes) + + [General Features](#general-features) + - [Locating persons and notes by tag : `findTag`](#locating-persons-and-notes-by-tag--findtag) + - [Inspecting a person : `inspect`](#inspecting-a-person--inspect) + - [Viewing help : `help`](#viewing-help--help) + - [Clearing all entries : `clear`](#clearing-all-entries--clear) + - [Exiting the program : `exit`](#exiting-the-program--exit) + - [Saving the data](#saving-the-data) + - [Editing the data file](#editing-the-data-file) + - [Archiving data files `[coming in v2.0]`](#archiving-data-files-coming-in-v20) + - [Undo `[coming in v2.0]`](#undo-coming-in-v20) + - [Redo `[coming in v2.0]`](#redo-coming-in-v20) + * [FAQ](#faq) + * [Glossary](#glossary) + * [Summary](#summary) + +--- + +## Introduction to SectresBook + +**SectresBook** is a desktop application that helps you to manage information about your club members (such as loans and birthdays) and keep track of your tasks using notes. + +**SectresBook** provides these main features: +* Adding a club member. +* Accessing, modifying members' information. +* Adding a note. +* Accessing, modifying contents of a note. +* Tagging a club member or a note (or both) for organisation + +
+SectresBook is already a convenient way to keep track of information you need to manage a club. However, if you can type fast, using SectresBook will be quicker and more efficient. +
+ +[Back to Table of Contents](#table-of-contents) + +--- + +## Using this guide + +This user guide contains all the information that you will need to use and learn **SectresBook**. + +If you are a **new user**, the necessary knowledge for you to get started can be found [here](#quick-start). + +If you are an **experienced user**, a [Summary](#summary) is also provided, so you can quickly refer to the command formats. + +Before you delve into the guide, do take note of the following highlighted information panels. + +
:information_source: **Note:** Used to highlight and display information you should +pay attention to.
+ +
:bulb: **Tip:** used to highlight tips which you might find useful
+ +
:exclamation: **Caution:** used to highlight dangers and things to look out for.
+ +In addition, for better readability, icons in this guide have been colored black. In the actual application, colors may be inverted, but their shape will remain the same. + +
:bulb: **Tip:** This User Guide contains many clickable links. Use the keyboard shortcuts Alt + Left arrow and Alt + Right arrow to navigate back and forth between links quickly. (Command + Left arrow and Command + Right arrow for Mac)
+ +[Back to Table of Contents](#table-of-contents) + +--- ## Quick start -1. Ensure you have Java `11` or above installed in your Computer. +1. Ensure you have Java `11`[7](#glossary) or above installed in your Computer. + +1. Download the latest `SectresBook.jar` from [here](https://github.com/AY2223S1-CS2103T-W12-2/tp/releases). + +1. Copy the file to the folder you want to use as the _home folder_[5](#glossary) for your SectresBook. + +1. Double-click the file to start the app. A GUI[4](#glossary) similar to the one below should appear in a few seconds. Note how the app contains some sample data. (Don't worry about the layout of the GUI[4](#glossary) yet! It will be explained in the next section.) + ![Ui](images/Ui.png) + +1. We **strongly recommend** you to use this app at a resolution of 1024x640 or greater to experience the greatest level of comfort. You may also click the fullscreen icon at the top right hand corner of the window next to the close icon to maximise the window. + +1. Press your spacebar (recommended), or bring your cursor to the [command box](#command-box) area and click onto it, to access the typing space. The results display will come into view below the command box. + +1. Type commands in the [command box](#command-box) and press Enter to execute them. For example, typing **`help`** into the command box and pressing Enter will open the [help window](#viewing-help--help). + - Some example commands you can try: + - **`list`** : Lists all contacts. + - add name/John Doe phone/98765432 email/johnd@example.com home/John street, block 123, #01-01 bday/01/01/2000 : Adds a contact named `John Doe` to the SectresBook. + - **`delete 3`** : Deletes the 3rd contact shown in the current list. + - **`help`** : Opens a [help window](#viewing-help--help). + - **`clear`** : Deletes all contacts. + - **`exit`** : Exits the app. + +1. Once you are done executing commands and want to see the GUI, press the `ESC` key (recommended), or click on anywhere that is not the command box, to leave the typing space and hide the results display. + +Refer to [Features](#features) below for details of each command. + +[Back to Table of Contents](#table-of-contents) + +--- + +## User Interface + +Here is an overview of the User Interface (UI) components. + +The UI comprises four sections: + +![UILabeled](images/UiLabeled.png) + +### Command Box + +The Command Box is where you type in your command inputs. For more information on command inputs, refer to [Features](#features) below. + +Once the command box is selected, a results display will appear to report the status of the program to you. Error messages and success messages will be shown in this box. Click anywhere else on the screen, or press the `ESC` key to exit the command box and hide the results display. + +

+ +

+
Results Display.
+ +
+ +
:bulb: **Tip:** +You may activate the command box by simply pressing the spacebar on your keyboard. There is no need to use your mouse to click on the bar. +
+ +
:bulb: **Tip:** +Similarly, you may press the `ESC` key on your keyboard to exit out of the command box and hide the results display. +
+ +[Back to Table of Contents](#table-of-contents) + +### People Panel + +The People Panel contains all the club and organisation members you have registered in this book. They are laid out horizontally. You can scroll the list by hovering your mouse over the People Panel and scrolling the mouse-wheel, or by clicking on and dragging the horizontal scroll bar to scroll. + +Each card represents a person and displays their name, phone number and total present loan amount. The loan amount may be positive to indicate an amount owed by the person, or negative to indicate an amount due to be paid to the person. + +

+ +

+
A person card.
+ +The index of the person only applies to the currently displayed list, it is **not** tied to the person itself. + +Check [Person Features](#person-features) to learn more about the commands you can execute related to people. + +[Back to Table of Contents](#table-of-contents) + +### Inspect Panel + +The Inspect Panel is related to the People Panel and shows the basic information of the currently inspected person. A person can be inspected by either clicking on his or her card, or by using the `inspect` command. More details on the `inspect` command can be found [here](#inspecting-a-person--inspect). + +The left side of the Inspect Panel shows the basic information, while the right side shows the history of loan transactions. + +Note that the transaction record next to the icon of the hand holding coins is the most recent, and the earlier transactions are listed below. + +The total amount of the loans is also stated in the right of this panel, describing in fuller detail if the sum is owed by or to be paid to the person. + +[Back to Table of Contents](#table-of-contents) + +### Notes Panel + +This Notes Panel stores all the information related to notes and tasks that the user may want to keep track of. + +Each note contains an index, a title, contents and tags. + +

+ +

+
A note card.
+ +The index of the notes only applies to the currently displayed list, it is **not** tied to the note itself. + +Both the People Panel and Notes Panel share a pool of tags to more easily relate a group of people to a specific note. + +Check [Notes Features](#note-features) to learn more about the commands you can execute related to notes. + +[Back to Table of Contents](#table-of-contents) + +--- + +## Properties + +### Person Properties + +

+ +

+
A typical person card.
+ +#### Name + +This is the name of the person to be recorded in the SectresBook. Two persons cannot have exactly the same name, but slight deviations are acceptable. Our team recognizes that many people may share the same name, so it is alright to use close aliases. + +This property[12](#glossary) is compulsory to declare during initialisation[6](#glossary). + +Only alphanumeric characters[1](#glossary) are accepted in this property[12](#glossary). Numbers are accepted in use as disambiguation, but may not exist as a standalone word without alphabets immediately before or following it. See examples below for a more specific overview. + +This property[12](#glossary) can be identified in the GUI[4](#glossary) by the icon of the silhouette of a person. + + + +- Identified by the prefix `name`. +- This is a valid property[12](#glossary) to find a person by using the [`find` command](#locating-persons-by-name-or-contact-number--find). -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). + + + + + + + + + +
Valid Examples + Samuel West
+ Jonathan Lee Wen Xin
+ Jack Robert the 3rd
+ 3Lite M1k0ch1
+ Elizabeth Wong n11 +
Invalid Examples + @*)^% (Non-alphanumeric characters are not accepted)
+ Jack Robert the 3 (Number may not exist as a standalone word)
+ Elizabeth Wong 11 (Number may not exist as a standalone word)
+ Jonathan 25 Chin (Number may not exist as a standalone word)
+ (25) Jonathan Chin (Non-alphanumeric characters cannot be used adjacent to numbers)
+
-1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +
:bulb: **Tip:** +It is recommended to include the full name of the person instead of aliases for easier searching. +
+ +[Back to Table of Contents](#table-of-contents) + +#### Phone + +This is the phone number of the person to be recorded in the SectresBook. It is compulsory to declare this during initialisation[6](#glossary). + +This property[12](#glossary) can be identified in the GUI[4](#glossary) by the icon of a mobile phone. + + + +- Identified by the prefix `phone`. +- A phone number should contain only numbers and be at least 3 digits long. +- This is a valid property[12](#glossary) to find a person by using the [`find` command](#locating-persons-by-name-or-contact-number--find). + +Please record the phone number by which the member is most easily contacted. You are not allowed to enter multiple phone numbers in this field. + +
:exclamation: **Caution:** +Ensure that no two persons have the same phone number! This is allowed in the program, but you may have difficulties contacting the person you want in the future. +
+ +[Back to Table of Contents](#table-of-contents) + +#### Email + +This is the email address of the person to be recorded in the SectresBook. It serves mainly as a point of information regarding the person, and has no additional features tied to it. This property[12](#glossary) is compulsory to declare during initialisation[6](#glossary). + +This property[12](#glossary) can be identified in the GUI[4](#glossary) by the icon of an envelope. + + + +- Identified by the prefix `email`. +- Emails should be of the format `local-part@domain` and adhere to the following constraints: + +| Part | Constraint | +|------|-------------| +| Local part | The local-part should only contain alphanumeric characters[1](#glossary) and these special characters `_`, `.`, `+` and `-`.

The local-part may not start or end with any special characters and special characters may not be adjacent to each other. + Domain name | The domain name is made up of domain labels separated by periods. The domain name must:
- end with a domain label at least 2 characters long
- have each domain label start and end with alphanumeric characters[1](#glossary)
- have each domain label consist of alphanumeric characters[1](#glossary), separated only by hyphens, if any. + +The local part and domain part **must** be connected by an `@` symbol. + +[Back to Table of Contents](#table-of-contents) + +#### Address +This is the residing address of the person. It serves mainly as a point of information regarding the person, and has no additional features tied to it. This property[12](#glossary) is compulsory to declare during initialisation[6](#glossary). + +This property[12](#glossary) can be identified in the GUI[4](#glossary) by the icon of a house. + + + +- Identified by the prefix `home`. +- There is no constraint on how the home address of a person should be written, as long as it is sufficiently understandable. + +
:information_source: **Note:** +Please ensure that your SectresBook window can accommodate the length of the text. If you find the text being cut off and see ellipses `...` showing, please resize the SectresBook window to fit the text. +
+ +[Back to Table of Contents](#table-of-contents) + +#### Loan +This is amount of money that is owed by a person, or is to be paid to that person. This is a property[12](#glossary) that cannot be manipulated directly, but can only be edited with the [`editLoan` command](#editing-loan-of-a-person--editloan). + +This property[12](#glossary) can be identified in the GUI[4](#glossary) by the icon of the hand holding two stacks of coins. + + + +- A loan amount can be either negative, positive or zero. + + A positive value indicates an amount that the person has yet to pay to the organisation. + + A zero value indicates no outstanding loan. + + A negative value indicates an amount that should be paid back to the person. +- Loans can only take up numerical values. +- The maximum value this property[12](#glossary) can take is `$1,000,000,000,000.00` (1 trillion dollars ). Similarly, the minimum value is `-$1,000,000,000,000.00` (negative 1 trillion dollars). This should be more than sufficient for general purpose use. + +[Back to Table of Contents](#table-of-contents) + +#### Loan History +A loan history is linked to the loans properties and describes the changes to the numeric values of the loans in detail. There is no way to declare this during initialisation[6](#glossary), but it can be modified by the [`editLoan` command](#editing-loan-of-a-person--editloan). + +This property[12](#glossary) is represented as a descending list on the right side of the inspection panel. + +![img.png](images/LoanHistoryPanel.png) + +The most recent transaction is recorded at the top, on the row next to the loans icon. + +The upward pointing red arrow signifies an amount loaned to this member in a transaction. + + + +The downward pointing green arrow signifies an amount that this member has paid back in a transaction. + + + +- Consists of the following sub-properties: + - Current loan value + - Change in amount from last value + - Reason for change + +
:bulb: **Tip:** +Including a reason for every change to a person's loan value reduces the risk of accidentally adding an incorrect amount to someone. It keeps a detailed tab of every increment and decrement in value. +
-1. Double-click the file to start the app. The GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +[Back to Table of Contents](#table-of-contents) -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try: +#### Birthday +The birthday date of a person. This property[12](#glossary) is compulsory to declare during initialisation[6](#glossary). - * **`list`** : Lists all contacts. +This property[12](#glossary) can be identified in the GUI[4](#glossary) by the icon of a birthday cake. - * **`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. + - * **`delete`**`3` : Deletes the 3rd contact shown in the current list. +- Identified by the prefix `bday`. +- Serves as a point of information for a person. +- This must be a valid date in the form `dd/MM/yyyy`. - * **`clear`** : Deletes all contacts. +[Back to Table of Contents](#table-of-contents) - * **`exit`** : Exits the app. +#### Tags +Persons can be linked to tag objects, which serve as markers that draw connections between different people as well as associated notes. This property[12](#glossary) is completely optional. -1. Refer to the [Features](#features) below for details of each command. +This property[12](#glossary) can be identified in the GUI[4](#glossary) by tag shaped labels with text inside them. They are usually located at the bottom right of each person card. If there is no such label, then it means this person has no associated tags. + +![img.png](images/PersonTags.png) + +
:bulb: **Tip:** +Choose short, identifiable tag names. +
+ +
:exclamation: **Caution:** +There is no limit to how many tags you can apply to an individual person, but applying too many tags increases confusion and decreases user experience. Please choose the most relevant tags to apply only. +
+ +
:bulb: **Tip:** +Please refer to the [`Tag Properties`](#tag-properties) below for more information regarding tags. +
+ +[Back to Table of Contents](#table-of-contents) + +### Notes Properties + +

+ +

+
A typical note card.
+ +#### Title +The title serves as the main marker for notes and summarises the important details of this specific note. This property[12](#glossary) is compulsory to declare during initialisation[6](#glossary). + +It is the first line of a note card, which is bigger than the rest of the text. + +Special characters are allowed in the title, but they will always be treated as a whitespace[17](#glossary) during operations. This is because punctuation is not normally a part of a word. This feature is relevant to [`findNote`](#locating-a-note-by-title--findnote). + +- This property[12](#glossary) is identified by the prefix `title`. +- Notes can be filtered through with the [`findNote` command](#locating-a-note-by-title--findnote) using the title property[12](#glossary). +- Titles must be within 100 characters and can contain any ASCII characters[2](#glossary). +- This property[12](#glossary) cannot be left empty. + +[Back to Table of Contents](#table-of-contents) + +#### Content +The content serves as the description for notes. This property[12](#glossary) is compulsory to declare during initialisation[6](#glossary). + +This is immediately below the title and is in a smaller font size. + +- This property[12](#glossary) is identified by `content`. +- This property[12](#glossary) cannot be left empty. + +[Back to Table of Contents](#table-of-contents) + +#### Tags + +Notes can be linked to tag objects, which serve as markers that draw connections between different people as well as associated notes. This is an optional property[12](#glossary) for all notes. + +This is located at the bottom right of the notes card. If there is no such label, then it means this note has no associated tags. + +![img.png](images/NoteTags.png) + +
:bulb: **Tip:** +Choose short, identifiable tag names. +
+ +
:bulb: **Tip:** +Please refer to the [`Tag Properties`](#tag-properties) section below for more information regarding tags. +
+ +[Back to Table of Contents](#table-of-contents) + +### Tag Properties +A tag is used to group together specific People and Notes. + +This way, searching for a tag brings up all the People and Notes that have the tag for easier classification of related information. + +- Identified by the prefix `tag`. +- Tags can only consist of alphanumeric characters[1](#glossary). +- Persons and Notes can hold tags. + +[Back to Table of Contents](#table-of-contents) -------------------------------------------------------------------------------------------------------------------- @@ -45,148 +476,676 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo **:information_source: Notes about the command format:**
* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. + e.g. in `add name/NAME`, `NAME` is a parameter which can be used as `add name/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 `name/NAME [tag/TAG]` can be used as `name/John Doe tag/friend` or as `name/John Doe`. + +* `` signifies an exclusive-or parameter that is to be input.
+ e.g `INDEX NAME` allows either the parameter `INDEX` or the parameter `NAME`, but not both * 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. + e.g. `[tag/TAG]…​` can be used as ` ` (i.e. 0 times), `tag/friend`, `tag/friend tag/family` etc. * Parameters can be in any order.
- e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. + e.g. if the command specifies `name/NAME phone/PHONE_NUMBER`, `phone/PHONE_NUMBER name/NAME` is also acceptable. * If a parameter is expected only once in the command but you specified it multiple times, only the last occurrence of the parameter will be taken.
- e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` will be taken. + e.g. if you specify `phone/12341234 phone/56785678`, only `phone/56785678` will be taken. * 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`.
-### Viewing help : `help` -Shows a message explaning how to access the help page. +### Person Features -![help message](images/helpMessage.png) +#### Adding a new person : `add` -Format: `help` +Adds a person to the SectresBook. The new member will appear at the end of the persons' list residing in the People Panel. +Format: `add name/NAME phone/PHONE_NUMBER email/EMAIL home/ADDRESS bday/BIRTHDAY [tag/TAG]...​` -### Adding a person: `add` +
:bulb: **Tip:** +A person can have any number of tags (including 0). +
-Adds a person to the address book. +Examples: +* `add name/John Doe phone/98765432 email/johnd@example.com home/John street, block 123, #01-01 bday/01/04/2010 tag/Member` +* `add name/Jane Doe phone/98876542 email/jane@example.com home/That Street, block 133, #11-10 bday/05/11/1986 tag/Member` +* `add name/Neethesh tag/VicePresident email/neethesh@example.com home/Happy Avenue phone/91234567 bday/24/05/1998` -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +[Back to Table of Contents](#table-of-contents) -
:bulb: **Tip:** -A person can have any number of tags (including 0) +#### Editing a club member : `edit` + +Edits an existing club member’s information in the SectresBook. + +Format: `edit INDEX NAME [name/NAME] [phone/PHONE] [email/EMAIL] [home/ADDRESS] [bday/BIRTHDAY] [tag/TAG]…​` + +Remember that all terms are optional for edit commands, but it must include at least one property[12](#glossary) to edit the person by. + +If ambiguities by the keywords given exist, the persons' list will auto-filter to display all matching persons that cause the ambiguity to arise. Please fine tune your search requirements, or use an index from here, to edit the person again. + +Example of usage: + +`edit 1 phone/99999999` can be used to easily update the first person's contact information. + +`edit John phone/91235555` can be used to update a person’s contact information if there exists only one person whose name contains John.
+ +
:information_source: **Note:** +If no person is named `John`, or if more than one person has `John` in their name, then the operation is equivalent to `find John`.
-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` +[Back to Table of Contents](#table-of-contents) -### Listing all persons : `list` +#### Editing loan of a person : `editLoan` -Shows a list of all persons in the address book. +Edits an existing club member's loan amount in the SectresBook. Please note that the value must be either positive or negative values with up to 2 decimal places. -Format: `list` +The absolute maximum amount for a loan is `$1,000,000,000,000.00` (positive or negative 1 trillion). If you are intending to file more than a trillion dollars in total transactions, this application may not be a suitable one for you, as our expected clients do not normally transfer this much money. -### Editing a person : `edit` +
:exclamation: **Caution:** +If the total amount after the `editLoan` command adds up to more than a trillion, the program will block the command from going through. You will be notified by the results display should such a command be attempted. +
-Edits an existing person in the address book. +Format: `editLoan INDEX NAME amt/VALUE reason/REASON` -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +* Edits the loan value of the existing 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 …​ +* The `VALUE` can be a positive or negative value with up to 2 decimal places. +* The `VALUE` may not contain delimiters to separate digits, i.e. `$1,000,000` must be expressed as `$1000000` in the program. +* The loan value will be changed by the value given i.e current loan + `VALUE`. + +Examples of usage: + +* `editLoan 2 amt/30 reason/bought logistics` +* `editLoan alex amt/-30 reason/return money from logistics` +* `list` followed by `editLoan 1 amt/-20 reason/return money` will edit the 1st person in the SectresBook, reducing their loan by $20 and saving the `REASON` as `return money`. + +
:information_source: **Note:** +In the second example, if no person is named `Alex`, or if more than one person has `Alex` in their name, then the operation is equivalent to `find alex`. +
-* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ -* At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +[Back to Table of Contents](#table-of-contents) + +#### Deleting a person : `delete` + +Deletes the specified person from the SectresBook. Delete commands are irreversible, so please ensure that you are deleting the correct person. + +Format: `delete INDEX NAME` + + + + + + + + + + +
+ Deleting by 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, …​
  • +
+
+ Deleting by NAME + +
    +
  • Delete the entry of the person by name with the given keyword, only if the keywords specified are unique to that person's name.
  • +
  • Will not perform any operation if the name of the person does not exist, but instead execute find NAME
  • +
  • If the SectresBook contains more than one person that can be found by the keyword specified, the delete command will not execute but will return a list of all people with the given name. From here, you may choose to delete by index.
  • +
+
Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +* `list` followed by `delete 2` deletes the 2nd person in the SectresBook. +* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. +* `delete Betsy` deletes the entry belonging to Betsy in the SectresBook. +* `delete Lynette` does not perform any operation, if Lynette does not exist in the SectresBook. + +
:bulb: **Tip:** +To delete everyone at the same time, please refer to the clear command. +
+ +[Back to Table of Contents](#table-of-contents) -### Locating persons by name: `find` +#### Locating persons by name or contact number : `find` -Finds persons whose names contain any of the given keywords. +Finds persons whose names match any of the given keywords, or persons whose phone numbers start with any of the given keywords (in digits). + +
:information_source: **Note:** +The People Panel will show a `FILTERED` indicator to inform you that the list has been filtered. +
+ + Format: `find KEYWORD [MORE_KEYWORDS]` -* The search is case-insensitive. e.g `hans` will match `Hans` +* The search is not case-sensitive. 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` +* Phone numbers starting with any of the given keywords (in digits), of at least 2 digits, will be returned. Examples: -* `find John` returns `john` and `John Doe` + * `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) + -### Deleting a person : `delete` -Deletes the specified person from the address book. +* `find 86` returns `Theodore`
+ + -Format: `delete INDEX` +* `find 8` will not be accepted as a command, as searching by phone numbers must be of at least 2 digits. -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. +[Back to Table of Contents](#table-of-contents) + +#### Listing all persons : `list` + +List of all persons in the SectresBook. + +
:information_source: **Note:** +If the list was formerly filtered, the filter icon to the right of the person's label will disappear. +
+ +Format: `list` + +[Back to Table of Contents](#table-of-contents) + +#### Sorting by property : `sort` `[coming in v2.0]` + +
:exclamation: **Caution:** +This command does not exist in the present version of the program that this User Guide is written for. It **will not work** if entered. +
+ +Sorts the members in the persons' list by properties in either ascending or descending order. + +[Back to Table of Contents](#table-of-contents) + + +### Note Features + +#### Adding Notes : `addNote` + +Adds a note to the SectresBook. + +Format: `addNote title/TITLE content/CONTENT [tag/TAG]... ` + +
:bulb: **Tip:** +TITLE must be unique and not longer than 100 characters. Tags are also optional. +
+ +Examples: +* `addNote title/Club meeting soon! content/Remind club members to attend meeting.` +* `addNote title/T-Shirt payment due content/Collect money tag/Juniors` + +[Back to Table of Contents](#table-of-contents) + +#### Editing Notes : `editNote` + +Edits an existing specified note in the SectresBook. + +Format: `editNote INDEX TITLE [title/TITLE] [content/CONTENT] [tag/TAG]...` + +Example of usage: + +* `editNote 1 content/Second club meeting` can be used to easily update the first note's contents. +* `editNote alumni title/2020 alumni meeting` can be used to amend a note with the title "2020 alumni meeting", only if it is the only note containing "alumni" in its title. + +[Back to Table of Contents](#table-of-contents) + +#### Deleting Notes : `deleteNote` + +Deletes the specified note from the SectresBook. This operation is irreversible, so please ensure that the note you are about to delete is the correct one. + +Format: `deleteNote INDEX` + +* Deletes the note at the specified `INDEX`. +* The index refers to the index number shown in the displayed note list. * The index **must be a positive integer** 1, 2, 3, …​ +Unlike other entities, you may not delete a note by name. This is to minimise errors in deletion as many notes may have similar names. + 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. +* `listNotes` followed by `deleteNote 2` deletes the second note in the SectresBook. + +[Back to Table of Contents](#table-of-contents) + +#### Locating a note by title : `findNote` + +Finds the notes whose titles match any of the given keywords. + +
:information_source: **Note:** +The Notes Panel will show a `FILTERED` indicator to inform you that the list has been filtered. +
+ + + +The search is not case-sensitive. For example, `meeting` will match any notes containing the word `Meeting`. As in find for persons, the order of the keywords also does not matter, and `Meeting Club` will also match a note called `Club Meeting`. + +Please be informed that only the title will be search. Contents will not be searched through this command. + +Although the title may contain special characters such as `,`, `.`, `?` or `!`, these special characters are not accepted as part of a keyword. This is because punctuation are not normally associated as part of a word. During the parsing of the [title](#title), special characters are treated as spaces, so adjacent segments of the word containing it are treated as separate words. + * The keywords `?!` and `t-shirt` will not be allowed, as they contain special characters. + * `2` will match `shirt 2` but will not match `shirt2`, as `shirt2` is an entire word by itself. + * To search for `Upcoming club meeting!`, `meeting!` is not allowed as a keyword as it contains a special character, but `meeting` is allowed. + * `tshirt` will not match `t–shirt` as `t-shirt` is now treated as two words, `t` and `shirt` with the special character `-` being treated as a spacing. + +Format: `findNote KEYWORD [MORE_KEYWORDS]` + +Examples: +* `findNote Meeting` returns `Club Meeting`, `Meeting!` and `Meeting 2` +* `findNote Soon` returns `Payment (soon)` + +[Back to Table of Contents](#table-of-contents) + +#### Listing Notes : `listNote` + +Shows a list of all notes in the SectresBook. + +
:information_source: **Note:** +If the notes list was previously filtered, the filter icon will disappear to indicate that you are now looking at the full list. +
+ +This command, unlike many others, does not take in any parameters[10](#glossary). + +Format: `listNote` + +Examples: +* `listNote` + +[Back to Table of Contents](#table-of-contents) + +#### Sorting Notes : `sortNotes` `[coming in v2.0]` + +
:exclamation: **Caution:** +This command does not exist in the present version of the program that this User Guide is written for. It **will not work** if entered. +
+ +Sorts the notes in either ascending or descending order by title. + +[Back to Table of Contents](#table-of-contents) + +#### Hiding notes panel : `hideNotes` + +Hides the notes panel to the right side of the screen if visible, otherwise, no operation is performed. Use this command if you do not wish to see the notes view while you are working with information related to the members only. -### Clearing all entries : `clear` +
:information_source: **Note:** +Even though the notes panel may be hidden, operations on notes will still work as normal. This is purely a visual feature. +
+ +Format: `hideNotes` + +Example: + +Initial view: +![before hiding note](images/hideNotes-before.png) + +After executing `hideNotes`: +![after hiding notes](images/hideNotes-after.png) + +[Back to Table of Contents](#table-of-contents) + +#### Showing notes panel : `showNotes` + +Slides the notes panel into view if hidden, otherwise, no operation is performed. + +Format: `showNotes` + +Example : + +Initial view: +![before showing notes](images/showNotes-before.png) + +After executing `showNotes`: +![after showing notes](images/showNotes-after.png) -Clears all entries from the address book. +[Back to Table of Contents](#table-of-contents) + +
:information_source: **Note:**
+**Q** Why are `hideNotes` and `showNotes` in plural, but the rest of the operations in singular nouns?

+**A** `hideNotes` and `showNotes` are a separate class of commands that are _UI-Centric_, meaning that they only operate on the user interface, as they only shift a panel in the UI, and do not modify any underlying data of the application. The pluarity of the command disambiguates its usage from the other commands that mutates data. For more information, please read our developer guide. + +

To ease interpretation, you may read `showNotes` as _"show the **notes panel**"_ or `hideNotes` as _"hide the **notes panel**"_, while `listNote` may be read as _"list each individual **note**"_, `findNote` as _"find each **note** matching"_ and `editNote` as _"edit this **note**"_. +

+ +### General Features + +#### Locating persons and notes by tag : `findTag` + +Finds People and Notes that have the given tags. Both the People Panel and Notes Panel will be updated synchronously with all entities that match the specifiers. + +
:information_source: **Note:** +Both the People Panel and Notes Panel will show a `FILTERED` indicator to inform you that both lists have been filtered. +
+ +Format: `findTag TAG [MORE_TAGS]` + +* The tag search is not case-sensitive. e.g `finance` will match `Finance` +* Only tags are searched. +* Only full words will be matched e.g. `Tech` will not match `Technology` +* Persons and Notes matching at least one tag will be returned (i.e. `OR` search). + e.g. `Operations Finance` will return + * Person `Alex Yeoh` (tag: Friends) (tag: Operations), + * Person `Charlotte Oliveiro` (tag: Colleagues) (tag: Finance), + * Note `Collect funds from operations team` (tag: Operations) + +Examples: +* `findTag Operations Finance` returns Person `Alex Yeoh`, Person `Charlotte Oliveiro` and Note `Collect funds from operations team` + ![result for 'findTag Operations Finance'](images/findTagOperationsFinance.png) + +[Back to Table of Contents](#table-of-contents) + +#### Inspecting a person : `inspect` + +Updates the Inspect Panel with the basic information and loan history of the person inspected. + +Inspection is a _UI-centric_ command that operates on the currently viewed person’s list, so you may only inspect those that are presently listed. + +If you wish to view the properties of anyone in the full list, please remember to specify `list` to clear the filter. + +
:information_source: **Note:** +If there are multiple people in the list satisfying the keywords given, it will, by default, inspect the first person that matches the keywords. + +You may wish to use more unique keywords to reduce ambiguity, or inspect by an index. +
+ +Format: `inspect INDEX NAME` + +Examples: +* `inspect 2` inspects the second person in the list of people +* `inspect Lynette` will attempt to find the first person called `Lynette` in the currently **filtered** persons' list and update the Inspect Panel with her information. + +
:exclamation: **Caution:** +If you are trying to inspect a person by name, who does not exist in the currently viewed person list, which may be filtered, it will not be successful. This is because `inspect` only works on the currently viewed list. +
+ +[Back to Table of Contents](#table-of-contents) + +#### Viewing help : `help` + +Shows a message explaining how to access the help page. + +![help message](images/helpMessage.png) + +Copy the link to your clipboard by clicking the `Copy URL` button and paste it in your favourite browser to come back to this page anytime. + +Format: `help` + +[Back to Table of Contents](#table-of-contents) + +#### Clearing all entries : `clear` + +Clears all entries of people and notes from the SectresBook. This command is irreversible. This effectively restarts the app from a blank slate. Format: `clear` -### Exiting the program : `exit` +[Back to Table of Contents](#table-of-contents) + +#### Exiting the program : `exit` Exits the program. Format: `exit` -### Saving the data +
:information_source: **Note:** +Your data is saved automatically. +
-AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +[Back to Table of Contents](#table-of-contents) -### Editing the data file +#### Saving the data -AddressBook data are saved as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +SectresBook data is saved in the hard disk automatically after any command that changes the data. There is no need to save manually. + +[Back to Table of Contents](#table-of-contents) + +#### Editing the data file + +SectresBook data is saved as a JSON file[8](#glossary) at `[JAR file location]/data/sectresbook.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. +If your changes to the data file makes its format invalid, SectresBook will discard all data and start with an empty data file at the next run.
-### Archiving data files `[coming in v2.0]` +[Back to Table of Contents](#table-of-contents) -_Details coming soon ..._ +#### Archiving data files `[coming in v2.0]` --------------------------------------------------------------------------------------------------------------------- +
:exclamation: **Caution:** +This command does not exist in the present version of the program that this User Guide is written for. It **will not work** if entered. +
+ +Copies the current data file as a new file in the `HOME_FOLDER_LOCATION/archives` folder. This file may be used as a saved checkpoint to load past information into the SectresBook. + +[Back to Table of Contents](#table-of-contents) + +#### Undo `[coming in v2.0]` + +
:exclamation: **Caution:** +This command does not exist in the present version of the program that this User Guide is written for. It **will not work** if entered. +
+ +Undoes the last command that the user has inputted, setting the state of the saved data to an earlier state. + +[Back to Table of Contents](#table-of-contents) + +#### Redo `[coming in v2.0]` + +
:exclamation: **Caution:** +This command does not exist in the present version of the program that this User Guide is written for. It **will not work** if entered. +
+ +Pushes the state of the program to the later state if it was set back to an earlier state by the `undo` command. If the present state is the most recent state, no operation is performed. + +
:information_source: **Note:** +Note that performing undo commands and inputting a new command after will set the present state to the most recent state. No redo commands may be performed subsequently. +
+ +[Back to Table of Contents](#table-of-contents) +-------------------------------------------------------------------------------------------------------------------- ## FAQ +[//]: # (**Q** I work in a highly classified/important organisation and we have large volumes of monetary transactions and tasks we need to keep track of. Can we use this version of the program to manage our operations?
) + +[//]: # (**A** No, the current version of _SectresBook_ is not designed for such intensive and critical operations. It does not support any form of encryption or security and its performance has not been tested rigorously against larger loads. We ***do not suggest*** using this program in places where information loss or leaks may result in catastrophic consequences.) + +**Q** I cannot see any images in this document, what is wrong?
+**A** You are likely viewing the document in dark mode. The images are in black as their main color, please view this document in light mode instead. + **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. +**A**: Install the app in the other computer and overwrite the empty data file it creates with the previous SectresBook data file. + +**Q**: Double-clicking the jar file does not open the jar file, why does this happen?
+**A**: Make sure that Java 11[7](#glossary) is installed on your computer. You may also open the terminal[15](#glossary) and type in `java -jar SectresBook.jar`. + +**Q** Do I need to have Java installed to run SectresBook?
+**A** Yes, SectresBook runs on Java and would require it to work. For more information on how to install Java 11[7](#glossary), visit this [website](https://docs.oracle.com/en/java/javase/11/install/overview-jdk-installation.html). + +**Q** What can I can do if the window size is too small?
+**A** Drag the window of the application with your mouse to enlarge it, or simply click the top right maximise icon of the window. + +**Q** Why can't I see the full address of the person I am inspecting?
+**A** Your window is too small to accommodate the full label text, please resize your window either wider or taller so that the full text can be displayed. + +**Q** I have entered a command, but there is no response from the program, why did this happen?
+**A** A critical error may have occurred. You may likely be able to continue your usage of the program. Please review your terminal[15](#glossary) output and file a bug report on our [issues page](https://github.com/AY2223S1-CS2103T-W12-2/tp/issues), detailing a description of the bug as well as the steps to reproduce, attaching screenshots if any. + +**Q** Do I require an internet connection to use SectresBook?
+**A** No, SectresBook fully works without the need for internet connection. + +**Q** I have an existing data file but the app does not show any of the information, what is wrong?
+**A** Your data file is corrupted. If you know how to, please open the JSON data file[8](#glossary) in a text editor and remove the offending data. Otherwise, please refer to someone who knows how to remedy a corrupted data file. + +**Q** Who can I contact for questions regarding this version of the program if the information I need cannot be found in this guide?
+**A** Please contact the person from whom you obtained this present version from, or contact any of our developers (links to Github): + + + + + + + + + +
Rui Han Pinran Ryan Neethesh Zhong Wei
+ +[Back to Table of Contents](#table-of-contents) -------------------------------------------------------------------------------------------------------------------- -## Command summary - -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +## Glossary +The definitions in this glossary are context-specific to this application. + +No. | Word | Definition +---|----------------------------------|------------------ +1 | **Alphanumeric Characters** | A combination of alphabetical and numerical characters +2 | **ASCII Characters** | Characters from the American Standard Code for Information Interchange character encoding standard +3| **Command Line Interface (CLI)** | A text-based interface that receives typed commands as input and returns textual feedback as output. +4| **Graphical User Interface (GUI)** | An image-based interface that is more visually appealing than a command-line interface and encapsulates information through the use of icons and images. +5| **Home Folder** | The folder that SectresBook is located in. It is also where SectresBook will store its data +6|**Initialisation** | The moment when a Person or Note is created and added into SectresBook. +7| **Java 11** | Eleventh version of the Java Platform, Standard Edition Development Kit (JDK). SectresBook requires this to be installed to run. +8| **JSON data file** | A data file that uses the JavaScript Object Notation format. +9| **Loan** | An amount of money that is borrowed by or owed to a person. A positive value signifies an amount owed by the person and a negative value signifies an amount to be paid to that person. +10| **Parameter** | A value passed as a section of a command, typically following a prefix. +11| **Prefix** | A signposting word that indicates the kind of property (i.e. name, email, address, etc), which typically follows immediately after the prefix, that is to be passed as a parameter. +12| **Property** | An identifiable feature a person or object has that sufficiently distinguishes it from other objects of the same kind. +13| **Secretary** | A person that manages the tasks and events related to the operations of an organisation. +14| **Tag** | A label that groups related people together, such that they can be referred to as a single encapsulated entity specified by the tag. +15| **Terminal** | A text-based interface to the computer +16| **Treasurer** | A person that manages the finances and monetary transactions related to the operations of an organisation. +17| **Whitespace** | A space character + +[Back to Table of Contents](#table-of-contents) + +-------------------------------------------------------------------------------------------------------------------- +## Summary + +### Person Properties + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Person Properties
PropertiesExamples
Compulsory PropertiesNameCaptain Daniel Patrick the 5th
Phone99123510
AddressCanopy Street 11 Blk 709, #20-112
Emailcapt_daniel.patrick-the+5th@example.com
Birthday09/12/2000
Optional PropertiesLoanCannot be declared, used by loan history
Loan HistoryAn amount of $55 with a reason of "To buy a porcelain vase"
TagsCoworkers; Friends; Operations
+ +[Back to Table of Contents](#table-of-contents) + +### Person Commands + +Action | Format | Examples +--------|----------------------|-------- +**Add a person** | `add name/NAME phone/PHONE_NUMBER email/EMAIL home/ADDRESS bday/BIRTHDAY [tag/TAG]…​` | `add name/James Ho phone/22224444 email/jamesho@example.com home/123, Clementi Rd, 1234665 bday/01/01/2000 tag/friend tag/colleague` +**Edit a person** | `edit INDEX NAME [name/NAME] [phone/PHONE] [email/EMAIL] [home/ADDRESS] [bday/BIRTHDAY] [tag/TAG]…​` | `edit 2 name/James Lee email/jameslee@example.com` +**Edit the loan of a person** | `editLoan INDEX NAME amt/VALUE reason/REASON` | `editLoan 1 amt/-20 reason/Buy Logistics` +**Delete a person** | `delete INDEX NAME` | `delete 3`
`delete Jane` +**Find a person** | `find KEYWORD [MORE_KEYWORDS]` | `find James Jake`
`find 8651` +**List every person** | `list` | `list` + +[Back to Table of Contents](#table-of-contents) + +### Notes Properties + + + + + + + + + + + + + + + + + + + + + + + + +
Notes Properties
PropertiesExamples
Compulsory PropertiesTitleOrganise a charity to raise money for the foundling hospital
ContentTarget $50,000 - by 23rd of December. We're aiming to attract at least 1000 contributors.
Optional PropertiesTagsCoworkers; Friends; Operations
+ +[Back to Table of Contents](#table-of-contents) + +### Notes Commands + +Action | Format | Examples +--------|-------------------|------------- +**Add a Note** | `addNote title/TITLE content/CONTENT [tag/TAG]...` | `addNote title/Create Excel Sheet content/Create sheet for blockchain department` +**Edit a Note** | `editNote INDEX TITLE [title/TITLE] [content/CONTENT] [tag/TAG]...` | `editNote 1 title/Check meeting availability tag/president` +**Delete a Note** | `deleteNote INDEX` | `deleteNote 1` +**Find a Note** | `findNote KEYWORD [MORE_KEYWORDS]` | `findNote meeting` +**List Every Note** | `listNote` | `listNote` +**Hide Notes Panel** | `hideNotes` | `hideNotes` +**Show Notes Panel** | `showNotes` | `showNotes` + +[Back to Table of Contents](#table-of-contents) + +### General Commands + +Action | Format | Examples +-------|---------------------------|----------| +**Find Tag** | `findTag TAG [MORE_TAGS]` | `findTag Operations Outreach` +**Inspect** | `inspect INDEX NAME` | `inspect 1` or `inspect Alex` +**Help** | `help` | `help` +**Clear all data** | `clear` | `clear` +**Exit** | `exit` | `exit` + +[Back to Table of Contents](#table-of-contents) diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..d8a0fa10725 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "SectresBook" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2223S1-CS2103T-W12-2/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..6700ae00931 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: "SectresBook"; font-size: 32px; } } diff --git a/docs/diagrams/AddNoteSequenceDiagram.puml b/docs/diagrams/AddNoteSequenceDiagram.puml new file mode 100644 index 00000000000..e2b485088c7 --- /dev/null +++ b/docs/diagrams/AddNoteSequenceDiagram.puml @@ -0,0 +1,93 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":AddNoteCommandParser" as AddNoteCommandParser LOGIC_COLOR +participant ":Title" as Note_Title LOGIC_COLOR +participant ":Content" as Content LOGIC_COLOR +participant ":Note" as Note LOGIC_COLOR +participant "a:AddNoteCommand" as AddNoteCommand 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("addNote n_t/Meeting \nn_c/3rd October 9pm") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("addNote n_t/Meeting \nn_c/3rd October 9pm") +activate AddressBookParser + +create AddNoteCommandParser +AddressBookParser -> AddNoteCommandParser +activate AddNoteCommandParser + +AddNoteCommandParser --> AddressBookParser +deactivate AddNoteCommandParser + +AddressBookParser -> AddNoteCommandParser : parse("n_t/Meeting \nn_c/3rd October 9pm") +activate AddNoteCommandParser + +create Note_Title +AddNoteCommandParser -> Note_Title +activate Note_Title + +Note_Title --> AddNoteCommandParser +deactivate Note_Title + +create Content +AddNoteCommandParser -> Content +activate Content + +Content --> AddNoteCommandParser +deactivate Content + +create Note +AddNoteCommandParser -> Note +activate Note + +Note --> AddNoteCommandParser +deactivate Note + +create AddNoteCommand +AddNoteCommandParser -> AddNoteCommand +activate AddNoteCommand + +AddNoteCommand --> AddNoteCommandParser : a +deactivate AddNoteCommand + +AddNoteCommandParser --> AddressBookParser : a +deactivate AddNoteCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddNoteCommandParser -[hidden]-> AddressBookParser +destroy AddNoteCommandParser + +AddressBookParser --> LogicManager : a +deactivate AddressBookParser + +LogicManager -> AddNoteCommand : execute() +activate AddNoteCommand + +AddNoteCommand -> Model : addNote() +activate Model + +Model --> AddNoteCommand +deactivate Model + +create CommandResult +AddNoteCommand -> CommandResult +activate CommandResult + +CommandResult --> AddNoteCommand +deactivate CommandResult + +AddNoteCommand --> LogicManager : result +deactivate AddNoteCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/DeleteNoteSequenceDiagram.puml b/docs/diagrams/DeleteNoteSequenceDiagram.puml new file mode 100644 index 00000000000..f8b2466f7a4 --- /dev/null +++ b/docs/diagrams/DeleteNoteSequenceDiagram.puml @@ -0,0 +1,77 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":DeleteNoteCommandParser" as DeleteNoteCommandParser LOGIC_COLOR +participant "d:DeleteNoteCommand" as DeleteNoteCommand 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("deleteNote 1") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("deleteNote 1") +activate AddressBookParser + +create DeleteNoteCommandParser +AddressBookParser -> DeleteNoteCommandParser +activate DeleteNoteCommandParser + +DeleteNoteCommandParser --> AddressBookParser +deactivate DeleteNoteCommandParser + +AddressBookParser -> DeleteNoteCommandParser : parse("1") +activate DeleteNoteCommandParser + +create DeleteNoteCommand +DeleteNoteCommandParser -> DeleteNoteCommand +activate DeleteNoteCommand + +DeleteNoteCommand --> DeleteNoteCommandParser : d +deactivate DeleteNoteCommand + +DeleteNoteCommandParser --> AddressBookParser : d +deactivate DeleteNoteCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +DeleteNoteCommandParser -[hidden]-> AddressBookParser +destroy DeleteNoteCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> DeleteNoteCommand : execute() +activate DeleteNoteCommand + +DeleteNoteCommand -> Model : getAddressBook() +activate Model + +Model --> DeleteNoteCommand +deactivate Model + +DeleteNoteCommand -> Model : deleteNote(noteToDelete) +activate Model + +Model --> DeleteNoteCommand +deactivate Model + +create CommandResult +DeleteNoteCommand -> CommandResult +activate CommandResult + +CommandResult --> DeleteNoteCommand +deactivate CommandResult + +DeleteNoteCommand --> LogicManager : result +deactivate DeleteNoteCommand + + + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/DeleteSequenceWithNameDiagram.puml b/docs/diagrams/DeleteSequenceWithNameDiagram.puml new file mode 100644 index 00000000000..891285c2c39 --- /dev/null +++ b/docs/diagrams/DeleteSequenceWithNameDiagram.puml @@ -0,0 +1,76 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR +participant ":FindCommand" as FindCommand LOGIC_COLOR +participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("delete David") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("delete David") +activate AddressBookParser + +create DeleteCommandParser +AddressBookParser -> DeleteCommandParser +activate DeleteCommandParser + +DeleteCommandParser --> AddressBookParser +deactivate DeleteCommandParser + +AddressBookParser -> DeleteCommandParser : parse("David") +activate DeleteCommandParser + +create FindCommand +DeleteCommandParser -> FindCommand +activate FindCommand +FindCommand --> DeleteCommandParser +deactivate FindCommand + +create DeleteCommand +DeleteCommandParser -> DeleteCommand +activate DeleteCommand + +DeleteCommand --> DeleteCommandParser : d +deactivate DeleteCommand + +DeleteCommandParser --> AddressBookParser : d +deactivate DeleteCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +DeleteCommandParser -[hidden]-> AddressBookParser +destroy DeleteCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> DeleteCommand : execute() +activate DeleteCommand + +DeleteCommand -> Model : deletePerson(1) +activate Model + +Model --> DeleteCommand +deactivate Model + +create CommandResult +DeleteCommand -> CommandResult +activate CommandResult + +CommandResult --> DeleteCommand +deactivate CommandResult + +DeleteCommand --> LogicManager : result +deactivate DeleteCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EditLoanSequenceDiagram.puml b/docs/diagrams/EditLoanSequenceDiagram.puml new file mode 100644 index 00000000000..2ec0d915bd2 --- /dev/null +++ b/docs/diagrams/EditLoanSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":EditLoanCommandParser" as EditLoanCommandParser LOGIC_COLOR +participant "d:EditLoanCommand" as EditLoanCommand 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("editLoan 1 amt/10 reason/logistics") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("editLoan 1 amt/10 reason/logistics") +activate AddressBookParser + +create EditLoanCommandParser +AddressBookParser -> EditLoanCommandParser +activate EditLoanCommandParser + +EditLoanCommandParser --> AddressBookParser +deactivate EditLoanCommandParser + +AddressBookParser -> EditLoanCommandParser : parse("1 amt/10 reason/logistics") +activate EditLoanCommandParser + +create EditLoanCommand +EditLoanCommandParser -> EditLoanCommand +activate EditLoanCommand + +EditLoanCommand --> EditLoanCommandParser : d +deactivate EditLoanCommand + +EditLoanCommandParser --> AddressBookParser : d +deactivate EditLoanCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditLoanCommandParser -[hidden]-> AddressBookParser +destroy EditLoanCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> EditLoanCommand : execute() +activate EditLoanCommand + +EditLoanCommand -> Model : deletePerson(1) +activate Model + +Model --> EditLoanCommand +deactivate Model + +create CommandResult +EditLoanCommand -> CommandResult +activate CommandResult + +CommandResult --> EditLoanCommand +deactivate CommandResult + +EditLoanCommand --> LogicManager : result +deactivate EditLoanCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EditNoteActivityDiagram.puml b/docs/diagrams/EditNoteActivityDiagram.puml new file mode 100644 index 00000000000..1c12b2954e6 --- /dev/null +++ b/docs/diagrams/EditNoteActivityDiagram.puml @@ -0,0 +1,31 @@ +@startuml +start +:User executes editNote command; + +:Parses the command; + +switch () + +case ( [invalid] ) + :Show parse + error message; + +case ([command given index/title]) + :Executes editNote + command; + + switch() + case ([valid index and descriptor]) + :Edit note with + new details; + + case ([else]) + :Show invalid note + index error message; + + endswitch + + +endswitch +stop +@enduml diff --git a/docs/diagrams/EditNoteSequenceDiagram.puml b/docs/diagrams/EditNoteSequenceDiagram.puml new file mode 100644 index 00000000000..7bf5145d5dc --- /dev/null +++ b/docs/diagrams/EditNoteSequenceDiagram.puml @@ -0,0 +1,110 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":EditNoteCommandParser" as EditNoteCommandParser LOGIC_COLOR +participant ":FindNoteCommandParser" as FindNoteCommandParser LOGIC_COLOR +participant "f:FindNoteCommand" as FindNoteCommand LOGIC_COLOR +participant "e:EditNoteCommand" as EditNoteCommand 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("edit meeting \ntitle/Club Meeting") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("edit meeting \ntitle/Club Meeting") +activate AddressBookParser + +create EditNoteCommandParser +AddressBookParser -> EditNoteCommandParser +activate EditNoteCommandParser + +EditNoteCommandParser --> AddressBookParser +deactivate EditNoteCommandParser + +AddressBookParser -> EditNoteCommandParser : parse("meeting \ntitle/Club Meeting") +activate EditNoteCommandParser + +create FindNoteCommandParser +EditNoteCommandParser -> FindNoteCommandParser +activate FindNoteCommandParser + +FindNoteCommandParser --> EditNoteCommandParser +deactivate FindNoteCommandParser + +EditNoteCommandParser -> FindNoteCommandParser : parse("meeting") +activate FindNoteCommandParser + +create FindNoteCommand +FindNoteCommandParser -> FindNoteCommand +activate FindNoteCommand + +FindNoteCommand --> FindNoteCommandParser +deactivate FindNoteCommand + +FindNoteCommandParser --> EditNoteCommandParser :f +deactivate FindNoteCommandParser + +EditNoteCommandParser -> FindNoteCommand : execute() +activate FindNoteCommand + +FindNoteCommand -> Model : updateFilteredNoteList(predicate) +activate Model + +Model --> FindNoteCommand +deactivate Model + + +FindNoteCommand --> EditNoteCommandParser +destroy FindNoteCommand + +create EditNoteCommand +EditNoteCommandParser -> EditNoteCommand +activate EditNoteCommand + +EditNoteCommand --> EditNoteCommandParser : e +deactivate EditNoteCommand + +EditNoteCommandParser --> AddressBookParser : e +deactivate EditNoteCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditNoteCommandParser -[hidden]-> AddressBookParser +destroy EditNoteCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> EditNoteCommand : execute() +activate EditNoteCommand + +EditNoteCommand -> Model : getFilteredNoteList() +activate Model + +Model --> EditNoteCommand +deactivate Model + +EditNoteCommand -> Model : setNote(noteToEdit, editedNote) +activate Model + +Model --> EditNoteCommand +deactivate Model + +create CommandResult +EditNoteCommand -> CommandResult +activate CommandResult + +CommandResult --> EditNoteCommand +deactivate CommandResult + +EditNoteCommand --> LogicManager : result +deactivate EditNoteCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EditSequenceDiagram.puml b/docs/diagrams/EditSequenceDiagram.puml new file mode 100644 index 00000000000..a43c69aacbb --- /dev/null +++ b/docs/diagrams/EditSequenceDiagram.puml @@ -0,0 +1,91 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":EditCommandParser" as EditCommandParser LOGIC_COLOR +participant ":FindCommandParser" as FindCommandParser LOGIC_COLOR +participant "f:FindCommand" as FindCommand LOGIC_COLOR +participant "d:EditCommand" as EditCommand 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("edit Lynette") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("edit Lynette") +activate AddressBookParser + +create EditCommandParser +AddressBookParser -> EditCommandParser +activate EditCommandParser + +EditCommandParser --> AddressBookParser +deactivate EditCommandParser + +AddressBookParser -> EditCommandParser : parse("Lynette") +activate EditCommandParser + +create FindCommandParser +EditCommandParser -> FindCommandParser : find("Lynette") +activate FindCommandParser + +create FindCommand +FindCommandParser -> FindCommand +activate FindCommand + +FindCommand --> FindCommandParser +deactivate FindCommand + +FindCommandParser -> EditCommandParser +deactivate FindCommandParser + +EditCommandParser -> FindCommand : execute() +activate FindCommand + +FindCommand --> EditCommandParser +destroy FindCommand + +create EditCommand +EditCommandParser -> EditCommand +activate EditCommand + +EditCommand --> EditCommandParser : d +deactivate EditCommand + +EditCommandParser --> AddressBookParser : d +deactivate EditCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditCommandParser -[hidden]-> AddressBookParser +destroy EditCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> EditCommand : execute() +activate EditCommand + +EditCommand -> Model : deletePerson(1) +activate Model + +Model --> EditCommand +deactivate Model + +create CommandResult +EditCommand -> CommandResult +activate CommandResult + +CommandResult --> EditCommand +deactivate CommandResult + +EditCommand --> LogicManager : result +deactivate EditCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/FindCommandSequence.puml b/docs/diagrams/FindCommandSequence.puml new file mode 100644 index 00000000000..13731f6eb12 --- /dev/null +++ b/docs/diagrams/FindCommandSequence.puml @@ -0,0 +1,65 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FindCommandParser" as FindCommandParser LOGIC_COLOR +participant ":FindCommand" as FindCommand LOGIC_COLOR +participant "predicate:NameContainsKeywordsPredicate" as NameContainsKeywordsPredicate LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute(find David) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(find) +activate AddressBookParser + +create FindCommandParser +AddressBookParser -> FindCommandParser +activate FindCommandParser + +create FindCommand +FindCommandParser -> FindCommand +activate FindCommand + +create NameContainsKeywordsPredicate +FindCommand -> NameContainsKeywordsPredicate +activate NameContainsKeywordsPredicate + +NameContainsKeywordsPredicate --> FindCommand +deactivate NameContainsKeywordsPredicate + +FindCommand --> FindCommandParser +deactivate FindCommand + +FindCommandParser --> AddressBookParser +deactivate FindCommandParser +FindCommandParser -[hidden]-> AddressBookParser +destroy FindCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> FindCommand :execute(model) +activate FindCommand + +FindCommand -> Model :updateFilteredPersonList(predicate) +activate Model + +Model --> FindCommand +deactivate Model + +FindCommand --> LogicManager +deactivate FindCommand +FindCommand -[hidden]-> LogicManager +destroy FindCommand + +<-- LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/FindTagSequenceDiagram.puml b/docs/diagrams/FindTagSequenceDiagram.puml new file mode 100644 index 00000000000..3432642a92f --- /dev/null +++ b/docs/diagrams/FindTagSequenceDiagram.puml @@ -0,0 +1,78 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FindTagCommandParser" as FindTagCommandParser LOGIC_COLOR +participant ":FindTagCommand" as FindTagCommand LOGIC_COLOR +participant ":PersonTagsContainsKeywordsPredicate" as PersonTagsContainsKeywordsPredicate LOGIC_COLOR +participant ":NoteTagsContainsKeywordsPredicate" as NoteTagsContainsKeywordsPredicate LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute(findTag Finance) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(findTag) +activate AddressBookParser + +create FindTagCommandParser +AddressBookParser -> FindTagCommandParser +activate FindTagCommandParser + +create FindTagCommand +FindTagCommandParser -> FindTagCommand +activate FindTagCommand + +create PersonTagsContainsKeywordsPredicate +FindTagCommand -> PersonTagsContainsKeywordsPredicate +activate PersonTagsContainsKeywordsPredicate + +PersonTagsContainsKeywordsPredicate --> FindTagCommand +deactivate PersonTagsContainsKeywordsPredicate + +create NoteTagsContainsKeywordsPredicate +FindTagCommand -> NoteTagsContainsKeywordsPredicate +activate NoteTagsContainsKeywordsPredicate + +NoteTagsContainsKeywordsPredicate --> FindTagCommand +deactivate NoteTagsContainsKeywordsPredicate + + +FindTagCommand --> FindTagCommandParser +deactivate FindTagCommand + +FindTagCommandParser --> AddressBookParser +deactivate FindTagCommandParser +FindTagCommandParser -[hidden]-> AddressBookParser +destroy FindTagCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> FindTagCommand :execute(model) +activate FindTagCommand + +FindTagCommand -> Model :updateFilteredPersonList() +activate Model + +Model --> FindTagCommand + +FindTagCommand -> Model :updateFilteredNoteList() + +Model --> FindTagCommand +deactivate Model + +FindTagCommand --> LogicManager +deactivate FindTagCommand +FindTagCommand -[hidden]-> LogicManager +destroy FindTagCommand + +<-- LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/InspectSequenceDiagram.puml b/docs/diagrams/InspectSequenceDiagram.puml new file mode 100644 index 00000000000..27fef6befc8 --- /dev/null +++ b/docs/diagrams/InspectSequenceDiagram.puml @@ -0,0 +1,107 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":MainWindow" as MainWindow LOGIC_COLOR +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":InspectCommandParser" as InspectCommandParser LOGIC_COLOR +participant ":InspectCommand" as InspectCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box UI UI_COLOR_T1 +participant ":ResultDisplay" as ResultDisplay LOGIC_COLOR +participant "personListView:ListView" as ListView LOGIC_COLOR +participant ":InspectionPanel" as InspectionPanel LOGIC_COLOR +end box + + +[-> MainWindow : executeCommand("inspect David") +activate MainWindow + +MainWindow -> LogicManager : execute"("inspect David") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("inspect David") +activate AddressBookParser + +create InspectCommandParser +AddressBookParser -> InspectCommandParser +activate InspectCommandParser + +create InspectCommand +InspectCommandParser -> InspectCommand : parse("David") +activate InspectCommand + +create CommandResult +InspectCommand -> CommandResult : CommandResult(inspectionMessage, UIState.Inspect, ["David"]) +activate CommandResult + + +CommandResult --> InspectCommand +deactivate CommandResult + +InspectCommand --> InspectCommandParser : commandResult +deactivate InspectCommand +InspectCommand -[hidden]-> InspectCommandParser +destroy InspectCommand + +InspectCommandParser --> AddressBookParser : commandResult +deactivate InspectCommandParser +InspectCommandParser -[hidden]-> AddressBookParser +destroy InspectCommandParser + +AddressBookParser --> LogicManager : commandResult +deactivate AddressBookParser + +LogicManager --> MainWindow : commandResult +deactivate LogicManager + +MainWindow -> CommandResult : getFeedbackToUser() +activate CommandResult + +CommandResult --> MainWindow : feedback +deactivate CommandResult + +MainWindow -> ResultDisplay : setFeedbackToUser(feedback) +activate ResultDisplay +ResultDisplay -[hidden]-> MainWindow +deactivate ResultDisplay + +MainWindow -> CommandResult : getUiState() +activate CommandResult +CommandResult --> MainWindow : UIState.Inspect +deactivate CommandResult + +MainWindow -> CommandResult : getArgs() +activate CommandResult +CommandResult --> MainWindow : ["David"] +deactivate CommandResult + +MainWindow -> MainWindow : handleInspect(["David"]) + +MainWindow -> ListView : find 1st relevant person matching "David" +activate ListView + + +ListView -> InspectionPanel : setInspectParameters() +activate InspectionPanel + +ListView -[hidden]-> InspectionPanel +ListView -[hidden]-> InspectionPanel +ListView -[hidden]-> InspectionPanel +deactivate InspectionPanel + +ListView -> ResultDisplay +activate ResultDisplay + +ResultDisplay --[hidden]> ListView +deactivate ResultDisplay + +deactivate ListView + +<-- MainWindow +deactivate MainWindow + +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 4439108973a..dd5da9afcd4 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -13,12 +13,20 @@ Class ModelManager Class UserPrefs Class UniquePersonList +Class UniqueTagMapping Class Person Class Address +Class Birthday +Class Loan +Class LoanHistory Class Email Class Name Class Phone Class Tag +Class NoteBook +Class Note +Class Content +Class Title } @@ -28,23 +36,35 @@ HiddenOutside ..> Model AddressBook .up.|> ReadOnlyAddressBook ModelManager .up.|> Model -Model .right.> ReadOnlyUserPrefs -Model .left.> ReadOnlyAddressBook +Model ..> ReadOnlyUserPrefs +Model ..> ReadOnlyAddressBook ModelManager -left-> "1" AddressBook -ModelManager -right-> "1" UserPrefs +ModelManager -up-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList +AddressBook *---> "1" UniquePersonList +AddressBook *---> "1" UniqueTagMapping +AddressBook *---> "1" NoteBook UniquePersonList --> "~* all" Person +UniqueTagMapping ---> "~* all" Tag +NoteBook --> Note +Note *--> Content +Note *--> Title +Note *--> "1" Tag Person *--> Name Person *--> Phone Person *--> Email Person *--> Address -Person *--> "*" Tag +Person *--> Birthday +Person *--> Loan +Person *--> LoanHistory +Person "*" <--> "*" Tag Name -[hidden]right-> Phone Phone -[hidden]right-> Address Address -[hidden]right-> Email +NoteBook -[hidden]right-> UniquePersonList ModelManager -->"~* filtered" Person +ModelManager -->"~* filtered" Note @enduml diff --git a/docs/diagrams/NoteClassDiagram.puml b/docs/diagrams/NoteClassDiagram.puml new file mode 100644 index 00000000000..62de706bcb5 --- /dev/null +++ b/docs/diagrams/NoteClassDiagram.puml @@ -0,0 +1,12 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + + +Note *--> Title +Note *--> Content +Note "*" <--> "*" Tag +Person "1..*" <-up-> "*" Tag +@enduml diff --git a/docs/diagrams/PersonClassDiagram.puml b/docs/diagrams/PersonClassDiagram.puml new file mode 100644 index 00000000000..8593c575507 --- /dev/null +++ b/docs/diagrams/PersonClassDiagram.puml @@ -0,0 +1,18 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Person "1..*" <--> "*" Tag + +Person *--> Name +Person *--> Phone +Person *--> Email +Person *--> Address +Person *--> Birthday +Person *-right-> Loan +Person *--right-> "*" LoanHistory +LoanHistory --> "1" Loan +LoanHistory --> "1" Reason +@enduml diff --git a/docs/diagrams/UIActivityDiagram.puml b/docs/diagrams/UIActivityDiagram.puml new file mode 100644 index 00000000000..22fde66333d --- /dev/null +++ b/docs/diagrams/UIActivityDiagram.puml @@ -0,0 +1,56 @@ +@startuml +start + :User Interface is loaded; +repeat + + switch () + + case () + if () then ([User presses SPACE]) + :CommandBox captures the event tied + to the space key; + else ([User clicks on the CommandBox]) + endif + :ResultDisplay opacity increases + from 0 to 0.8 over the period of 0.3 seconds; + :User types in command; + :User presses the enter button; + + if () + ->[User typed in "exit"] ; + stop + else () + :Execute command; + endif + + case ([User clicks on anywhere outside of CommandBox]) + if () then ([ResultDisplay is shown]) + :ResultDisplay opacity is reduced from + 0.8 to 0 over a period of 0.3 seconds; + else ([ResultDisplay is hidden]) + endif + + case ( [User clicks on any PersonCard]) + :Selection Model of the ListView of Persons + is updated to the selected person; + :Inspection Panel captures the change in + Selection Model; + :Inspection Panel updates the labels with the + selection Person's particulars; + + endswitch + + + 'Since the beta syntax does not support placing the condition outside the + 'diamond we place it as the true branch instead. + + if () then ([valid command]) + :ResultDisplay shows command result message; + :UI updates state based on UIState and arguments passed; + else ([else]) + :ResultDisplay shows invalid command result message; + endif + repeat while (more commands?) is ([yes]) + ->[User presses the cross icon on the window]; +stop +@enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..0e53dffcf3a 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -6,15 +6,18 @@ skinparam classBackgroundColor UI_COLOR package UI <>{ Class "<>\nUi" as Ui -Class "{abstract}\nUiPart" as UiPart Class UiManager Class MainWindow Class HelpWindow Class ResultDisplay Class PersonListPanel +Class NoteListPanel Class PersonCard -Class StatusBarFooter +Class NoteCard Class CommandBox +Class InspectPanel +Class LoanHistoryCard +Class WindowAnchorPane } package Model <> { @@ -30,31 +33,29 @@ HiddenOutside ..> Ui UiManager .left.|> Ui UiManager -down-> "1" MainWindow + MainWindow *-down-> "1" CommandBox -MainWindow *-down-> "1" ResultDisplay -MainWindow *-down-> "1" PersonListPanel -MainWindow *-down-> "1" StatusBarFooter +MainWindow *-left-> "1" WindowAnchorPane MainWindow --> "0..1" HelpWindow -PersonListPanel -down-> "*" PersonCard - -MainWindow -left-|> UiPart +WindowAnchorPane *-down-> "1" ResultDisplay +WindowAnchorPane *-down-> "1" PersonListPanel +WindowAnchorPane *-down-> "1" NoteListPanel +WindowAnchorPane *-down-> "1" InspectPanel -ResultDisplay --|> UiPart -CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart -StatusBarFooter --|> UiPart -HelpWindow --|> UiPart +PersonListPanel -down-> "*" PersonCard +NoteListPanel -down-> "*" NoteCard +InspectPanel -down-> "*" LoanHistoryCard PersonCard ..> Model +NoteCard ..> Model +LoanHistoryCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic PersonListPanel -[hidden]left- HelpWindow HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay -ResultDisplay -[hidden]left- StatusBarFooter MainWindow -[hidden]-|> UiPart @enduml diff --git a/docs/images/AddNoteSequenceDiagram.png b/docs/images/AddNoteSequenceDiagram.png new file mode 100644 index 00000000000..81849155051 Binary files /dev/null and b/docs/images/AddNoteSequenceDiagram.png differ diff --git a/docs/images/DeleteNoteSequenceDiagram.png b/docs/images/DeleteNoteSequenceDiagram.png new file mode 100644 index 00000000000..c9a4ffb6b2f Binary files /dev/null and b/docs/images/DeleteNoteSequenceDiagram.png differ diff --git a/docs/images/DeleteSequenceWithNameDiagram.png b/docs/images/DeleteSequenceWithNameDiagram.png new file mode 100644 index 00000000000..1a077fd4388 Binary files /dev/null and b/docs/images/DeleteSequenceWithNameDiagram.png differ diff --git a/docs/images/EditLoanSequenceDiagram.png b/docs/images/EditLoanSequenceDiagram.png new file mode 100644 index 00000000000..19e3f0b0f8b Binary files /dev/null and b/docs/images/EditLoanSequenceDiagram.png differ diff --git a/docs/images/EditNoteActivityDiagram.png b/docs/images/EditNoteActivityDiagram.png new file mode 100644 index 00000000000..13864c2d48b Binary files /dev/null and b/docs/images/EditNoteActivityDiagram.png differ diff --git a/docs/images/EditNoteSequenceDiagram.png b/docs/images/EditNoteSequenceDiagram.png new file mode 100644 index 00000000000..dfef1db30fd Binary files /dev/null and b/docs/images/EditNoteSequenceDiagram.png differ diff --git a/docs/images/EditSequenceDiagram.png b/docs/images/EditSequenceDiagram.png new file mode 100644 index 00000000000..e841d9e97b5 Binary files /dev/null and b/docs/images/EditSequenceDiagram.png differ diff --git a/docs/images/FindCommandSequence.png b/docs/images/FindCommandSequence.png new file mode 100644 index 00000000000..b61ede7739d Binary files /dev/null and b/docs/images/FindCommandSequence.png differ diff --git a/docs/images/FindTagSequenceDiagram.png b/docs/images/FindTagSequenceDiagram.png new file mode 100644 index 00000000000..9d99897d248 Binary files /dev/null and b/docs/images/FindTagSequenceDiagram.png differ diff --git a/docs/images/InspectSequenceDiagram.png b/docs/images/InspectSequenceDiagram.png new file mode 100644 index 00000000000..cff6086b59d Binary files /dev/null and b/docs/images/InspectSequenceDiagram.png differ diff --git a/docs/images/LoanHistoryPanel.png b/docs/images/LoanHistoryPanel.png new file mode 100644 index 00000000000..a1343d85b7e Binary files /dev/null and b/docs/images/LoanHistoryPanel.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 04070af60d8..d166be8291c 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/NoteClassDiagram.png b/docs/images/NoteClassDiagram.png new file mode 100644 index 00000000000..a81e234200b Binary files /dev/null and b/docs/images/NoteClassDiagram.png differ diff --git a/docs/images/NoteTags.png b/docs/images/NoteTags.png new file mode 100644 index 00000000000..22a69387cb9 Binary files /dev/null and b/docs/images/NoteTags.png differ diff --git a/docs/images/NotesFilteredIcon.png b/docs/images/NotesFilteredIcon.png new file mode 100644 index 00000000000..ce14b82fd5c Binary files /dev/null and b/docs/images/NotesFilteredIcon.png differ diff --git a/docs/images/PeopleFilteredIcon.png b/docs/images/PeopleFilteredIcon.png new file mode 100644 index 00000000000..ca3d723b641 Binary files /dev/null and b/docs/images/PeopleFilteredIcon.png differ diff --git a/docs/images/PersonClassDiagram.png b/docs/images/PersonClassDiagram.png new file mode 100644 index 00000000000..061946bab55 Binary files /dev/null and b/docs/images/PersonClassDiagram.png differ diff --git a/docs/images/PersonTags.png b/docs/images/PersonTags.png new file mode 100644 index 00000000000..54e9774e749 Binary files /dev/null and b/docs/images/PersonTags.png differ diff --git a/docs/images/ResultsDisplay.png b/docs/images/ResultsDisplay.png new file mode 100644 index 00000000000..543057a07cd Binary files /dev/null and b/docs/images/ResultsDisplay.png differ diff --git a/docs/images/TypicalNoteCard.png b/docs/images/TypicalNoteCard.png new file mode 100644 index 00000000000..4dd52628db0 Binary files /dev/null and b/docs/images/TypicalNoteCard.png differ diff --git a/docs/images/TypicalPersonCard.png b/docs/images/TypicalPersonCard.png new file mode 100644 index 00000000000..fd877bb03c9 Binary files /dev/null and b/docs/images/TypicalPersonCard.png differ diff --git a/docs/images/UIActivityDiagram.png b/docs/images/UIActivityDiagram.png new file mode 100644 index 00000000000..a41b31ef31f Binary files /dev/null and b/docs/images/UIActivityDiagram.png differ diff --git a/docs/images/UIComponentsLabeled.png b/docs/images/UIComponentsLabeled.png new file mode 100644 index 00000000000..4eb1a84cc7b Binary files /dev/null and b/docs/images/UIComponentsLabeled.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..5624ac233ab 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 785e04dbab4..e66dc737c52 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UiLabeled.png b/docs/images/UiLabeled.png new file mode 100644 index 00000000000..eb3b2252b7e Binary files /dev/null and b/docs/images/UiLabeled.png differ diff --git a/docs/images/czhongwei.png b/docs/images/czhongwei.png new file mode 100644 index 00000000000..2ae5ef2911d Binary files /dev/null and b/docs/images/czhongwei.png differ diff --git a/docs/images/find86result.png b/docs/images/find86result.png new file mode 100644 index 00000000000..98906cf7695 Binary files /dev/null and b/docs/images/find86result.png differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png index 235da1c273e..ab27b081114 100644 Binary files a/docs/images/findAlexDavidResult.png and b/docs/images/findAlexDavidResult.png differ diff --git a/docs/images/findTagOperationsFinance.png b/docs/images/findTagOperationsFinance.png new file mode 100644 index 00000000000..c21a3f90a90 Binary files /dev/null and b/docs/images/findTagOperationsFinance.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..6c0ea259c4c 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/hideNotes-after.png b/docs/images/hideNotes-after.png new file mode 100644 index 00000000000..37eb5e2c6e6 Binary files /dev/null and b/docs/images/hideNotes-after.png differ diff --git a/docs/images/hideNotes-before.png b/docs/images/hideNotes-before.png new file mode 100644 index 00000000000..3da8c4dc0cd Binary files /dev/null and b/docs/images/hideNotes-before.png differ diff --git a/docs/images/icons/address_book_32.png b/docs/images/icons/address_book_32.png new file mode 100644 index 00000000000..3bea0752ed5 Binary files /dev/null and b/docs/images/icons/address_book_32.png differ diff --git a/docs/images/icons/bg.png b/docs/images/icons/bg.png new file mode 100644 index 00000000000..4e5cec32c27 Binary files /dev/null and b/docs/images/icons/bg.png differ diff --git a/docs/images/icons/birthday.png b/docs/images/icons/birthday.png new file mode 100644 index 00000000000..61ff2d89127 Binary files /dev/null and b/docs/images/icons/birthday.png differ diff --git a/docs/images/icons/calendar.png b/docs/images/icons/calendar.png new file mode 100644 index 00000000000..8b2bdf4f1c1 Binary files /dev/null and b/docs/images/icons/calendar.png differ diff --git a/docs/images/icons/clock.png b/docs/images/icons/clock.png new file mode 100644 index 00000000000..0807cbf6451 Binary files /dev/null and b/docs/images/icons/clock.png differ diff --git a/docs/images/icons/decrease_arrow.png b/docs/images/icons/decrease_arrow.png new file mode 100644 index 00000000000..ef9f53d5278 Binary files /dev/null and b/docs/images/icons/decrease_arrow.png differ diff --git a/docs/images/icons/fail.png b/docs/images/icons/fail.png new file mode 100644 index 00000000000..6daf01290dd Binary files /dev/null and b/docs/images/icons/fail.png differ diff --git a/docs/images/icons/filter.png b/docs/images/icons/filter.png new file mode 100644 index 00000000000..19c7571e6d8 Binary files /dev/null and b/docs/images/icons/filter.png differ diff --git a/docs/images/icons/help_icon.png b/docs/images/icons/help_icon.png new file mode 100644 index 00000000000..f8e80d6c1c5 Binary files /dev/null and b/docs/images/icons/help_icon.png differ diff --git a/docs/images/icons/home.png b/docs/images/icons/home.png new file mode 100644 index 00000000000..61093ae5525 Binary files /dev/null and b/docs/images/icons/home.png differ diff --git a/docs/images/icons/increase_arrow.png b/docs/images/icons/increase_arrow.png new file mode 100644 index 00000000000..fb7739be0c1 Binary files /dev/null and b/docs/images/icons/increase_arrow.png differ diff --git a/docs/images/icons/indicator_arrow.png b/docs/images/icons/indicator_arrow.png new file mode 100644 index 00000000000..8d81b2d072b Binary files /dev/null and b/docs/images/icons/indicator_arrow.png differ diff --git a/docs/images/icons/info_icon.png b/docs/images/icons/info_icon.png new file mode 100644 index 00000000000..f8cef714095 Binary files /dev/null and b/docs/images/icons/info_icon.png differ diff --git a/docs/images/icons/loan.png b/docs/images/icons/loan.png new file mode 100644 index 00000000000..0f4adeedb52 Binary files /dev/null and b/docs/images/icons/loan.png differ diff --git a/docs/images/icons/mail.png b/docs/images/icons/mail.png new file mode 100644 index 00000000000..49487a284f1 Binary files /dev/null and b/docs/images/icons/mail.png differ diff --git a/docs/images/icons/no_records.png b/docs/images/icons/no_records.png new file mode 100644 index 00000000000..9baa2f74743 Binary files /dev/null and b/docs/images/icons/no_records.png differ diff --git a/docs/images/icons/notebook.png b/docs/images/icons/notebook.png new file mode 100644 index 00000000000..c0691e3e632 Binary files /dev/null and b/docs/images/icons/notebook.png differ diff --git a/docs/images/icons/person.png b/docs/images/icons/person.png new file mode 100644 index 00000000000..a611c16cf4b Binary files /dev/null and b/docs/images/icons/person.png differ diff --git a/docs/images/icons/phone.png b/docs/images/icons/phone.png new file mode 100644 index 00000000000..aa56f805281 Binary files /dev/null and b/docs/images/icons/phone.png differ diff --git a/docs/images/neethesh26.png b/docs/images/neethesh26.png new file mode 100644 index 00000000000..129f11b6c6b Binary files /dev/null and b/docs/images/neethesh26.png differ diff --git a/docs/images/pinran-j.png b/docs/images/pinran-j.png new file mode 100644 index 00000000000..2ae5ef2911d Binary files /dev/null and b/docs/images/pinran-j.png differ diff --git a/docs/images/rui-han-crh.png b/docs/images/rui-han-crh.png new file mode 100644 index 00000000000..88942516341 Binary files /dev/null and b/docs/images/rui-han-crh.png differ diff --git a/docs/images/ryanczx.png b/docs/images/ryanczx.png new file mode 100644 index 00000000000..2ae5ef2911d Binary files /dev/null and b/docs/images/ryanczx.png differ diff --git a/docs/images/showNotes-after.png b/docs/images/showNotes-after.png new file mode 100644 index 00000000000..3fc9ed20536 Binary files /dev/null and b/docs/images/showNotes-after.png differ diff --git a/docs/images/showNotes-before.png b/docs/images/showNotes-before.png new file mode 100644 index 00000000000..707fa342bfa Binary files /dev/null and b/docs/images/showNotes-before.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..7be8226f2f5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,18 @@ --- layout: page -title: AddressBook Level-3 +title: SectresBook --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![CI Status](https://github.com/AY2223S1-CS2103T-W12-2/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2223S1-CS2103T-W12-2/tp/actions) +[![codecov](https://codecov.io/gh/AY2223S1-CS2103T-W12-2/tp/branch/master/graph/badge.svg)](https://codecov.io/gh/AY2223S1-CS2103T-W12-2/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). +**SectresBook** is a desktop application for club secretaries to (i) manage club member information, (ii) keep track of their tasks. -* 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 SectresBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing SectresBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/docs/team/czhongwei.md b/docs/team/czhongwei.md new file mode 100644 index 00000000000..5284fb36447 --- /dev/null +++ b/docs/team/czhongwei.md @@ -0,0 +1,47 @@ +--- +layout: page +title: Chee Zhong Wei's Project Portfolio Page +--- + +### Project: SectresBook + +SectresBook helps secretaries to maintain all the information of the members of their club by collating a list of identifiable information, past records and future tasks. + +Given below are my contributions to the project. + +* **Code contributed:** [RepoSense link](https://nus-cs2103-ay2223s1.github.io/tp-dashboard/?search=w12-2&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-09-16&tabOpen=true&tabType=authorship&zFR=false&tabAuthor=czhongwei&tabRepo=AY2223S1-CS2103T-W12-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + + +* **Enhancements implemented:** + * Enhancement: Locate persons by starting digits of phone number in `find` + * Originally, `find` only searches for a person that contains the whole keyword in their names + * Now, `find` also searches for a person with starting digits of the phone number (at least 2) that matches the keywords provided + * Enhancement: Added `Birthday` attribute to `Person` + * Each `Person` stores an additional information `Birthday`, which is in dd/mm/yyyy format + * The regular expression for `Birthday` includes checks for 29th February on leap years and invalid dates + * Enhancement: Changed prefixes of properties + * Changed prefixes of properties in commands to better differentiate them +
+ +* **Contributions to the UG:** + * Editted section to include enchancements : `find` + * Brief description and explanation + * Added screenshots for different features + * Wrote up the opening paragraph of the UG +
+ +* **Contributions to the DG:** + * Added content to `find` feature section: + * Brief description, explanation and design considerations + * Sequence diagram + * Added more User stories + * Added more Non-Functional Requirements + * Added more instructions for manual testing + * Did final polishing and checks for UG: + * Language use + * Spelling mistakes + * Formatting +
+ +* **Review/mentoring contributions**: + * Read through, checked and approved team PRs. diff --git a/docs/team/neethesh26.md b/docs/team/neethesh26.md new file mode 100644 index 00000000000..212eae7771d --- /dev/null +++ b/docs/team/neethesh26.md @@ -0,0 +1,85 @@ +--- +layout: page +title: T Neethesh's Project Portfolio Page +--- + +### Project: SectresBook + +Sectresbook helps secretaries to maintain all the information of the members of their club by collating a list +of identifiable information, past records and future tasks. + +Given below are my contributions to the project. + +### New Features Implemented + +--------------------------------- +1. Delete by Name + * What it does: Allows user to delete a contact from the Sectresbook by name rather than index. + * Justification: A secretary/treasurer might remember people by name rather than their indexes in the Sectresbook, hence might have a preference for deleting by name. Catering to that improves user experience. + * Highlights: Requires understanding of parser, ensuring that checks are done well when implementing the delete command. Difficult to ensure that a wrong person is not accidentally deleted and the number and name format does not overlap with each other. + * Credits: Neethesh [(Link to own PR)](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/57) + +2. Create Loan History + * What it does: Allows users to keep track of the changes in their loan amount and the reason for those changes. + * Justification: As a treasurer/secretary it is important to be able to keep track of the changes in the loan amount of people in their committee and the reasons for these changes for auditing purposes. + * Highlights: Difficult task as it requires full understanding of the code and its structure. Having to understand how the data is stored in `JsonSerializableAddressBook`. Required the modification/creation of over 20 files. Ensuring test cases aligned to the creation of the new field of a person as well. + * Credits: Neethesh [(Link to own PR)](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/99) + +3. Edit Loan command + * What it does: Allows the user to change the loan amounts by X amount with a reason. + * Justification: As a secretary/treasurer, the loan amounts of specific people will change often due to the usage and returning of funds of the committee. Hence, it is important to be able to change these amounts with the respective reasons for the changes being tracked. + * Highlights: Have to ensure that the inputs for the editLoan command for the amount to be changed is an integer that fits, and the reason is alphanumeric characters. + * Credits: Neethesh [(Link to own PR)](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/99) + + +### Code contributed + +----------------------------------- + +[RepoSense link](https://nus-cs2103-ay2223s1.github.io/tp-dashboard/?search=w12-2&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-09-16&tabOpen=true&tabType=authorship&zFR=false&tabAuthor=Neethesh26&tabRepo=AY2223S1-CS2103T-W12-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +### Project management + +----------------------------------- + +* Ensured meetings at stipulated timings. +* Approved PRs. +* Facilitated discussion and teamwork during meetings. +* Advocated asking for help when stuck with code. + + +### Contributions to the UG + +---------------------------------- + +* Added description for `delete` command to include delete by name. +* Added `editLoan` command description. +* Compiled and formatted the Summary portion for the commands. + +
+ +### Contributions to the DG + +----------------------------------- + +* Added the sequence diagram for `Delete` command, updating the description to include delete by name. +![DeleteSequenceWithNameDiagram](../images/DeleteSequenceWithNameDiagram.png) + + +* Added the section, description and sequence diagram for the editLoan command. + ![EditLoanSequenceDiagram](../images/EditLoanSequenceDiagram.png) + +* Compiled and added the use cases. + +* Compiled the `Appendix: Instructions for manual testing` section, adding the commands for testing and the description for majority of the features. + + +### Team based contributions + +-------------------------------- + +* Necessary implementation of Loan and Loan History feature, which is the biggest selling point of our product, where secretaries/treasurers are able to keep track of the individual amounts and changes. +* Updated parts of UG/DG not specific to a feature. +* Recognised and fixed bugs relating to features implemented and other features(Including UG bugs) + + diff --git a/docs/team/pinran-j.md b/docs/team/pinran-j.md new file mode 100644 index 00000000000..3f0196be758 --- /dev/null +++ b/docs/team/pinran-j.md @@ -0,0 +1,119 @@ +--- +layout: page +title: Pinran's Project Portfolio Page +--- + +### Project: SectresBook + +SectresBook helps secretaries to maintain all the information of the members of their club by collating a list of identifiable information, past records and future tasks. + +Given below are my contributions to the project. +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2223s1.github.io/tp-dashboard/?search=pinran-j&breakdown=true) + +* **New Features Implemented** + + 1. **_Notes model_** [(Link to own PR)](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/54) + * **What it does**: Add structure of notes in model. Notes contain a unique title and its content. Notes are contained in a `NoteBook`. + * **Justification**: A treasurer/secretary may require keeping notes on his/her tasks. + * **Highlights**: Difficult to implement as Notes interact with many parts of the code which requires full understanding of the code and its structure.(From `AddressBook`, `Model`, `ModelManger`, to how data is stored in `JsonSerializableAddressBook`). Required the modification/creation of over 15 classes. + + 2. **_Add Note command_** [(Link to own PR)](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/54) + * **What it does**: Allows adding of notes into SectresBook by specifying a title and content (optional tag) using respective prefixes. + * **Justification**: Command is needed to allow the user to add a note into SectresBook to be kept track of. + + 3. **_Delete Note command_** [(Link to own PR)](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/54) + * **What it does**: Allows the deleting of notes in SectresBook by specifying an index in the notes listed. + * **Justification**: A delete command is needed to allow users to delete notes that are no longer relevant or needed to be kept track of. + + 4. **_List Note command_** [(Link to own PR)](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/54) + * **What it does**: Lists all the notes that is being kept track of. + * **Justification**: A list command is needed to allow users at any point to view/reset the full list of notes. + + 5. **_Edit Note command_** [(Link to own PR)](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/81) + * **What it does**: Edits the note specified by index/name in the list of notes being kept track of. Notes can be edited by any combinations of `Title`, `Content` or `Tag` (At least one). + * **Justification**: An edit command is important to allow users to change a certain property of a note (In the event that a mistake was made when adding a note or a note's property changed) without the need of deleting the note and re-adding it. + * **Highlights**: Editing of notes was difficult as the properties of a Note cannot be directly accessed and modified, thus, a system needed to be in place in creating a new `Note` that needed to replace the `Note` to be edited. This new `Note` had to retain properties of the old `Note` that were not modified. + + 6. **_Find Note command_** [(Link to own PR)](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/81) + * **What it does**: Allow the user to find a note in the list of notes kept in SectresBook that matches the keyword specified. + * **Justification**: Find command for notes is important to allow users to filter through the current list of notes to magnify and search for the note that are of interest to them. This is important especially in the case where many notes are currently being kept track of. + * **Highlights**: Searching by keywords that matches words in `Title` of `Note` was tricky as `Title` contain special characters (ASCII). Thus, many design considerations were needed in deciding what should and should not be allowed to be keyed in as the keywords for the command. Keywords also made to ignore special characters, thus, special`String` manipulation was needed to allow for a correct search. + +* **Project management:** + * Initiated meetings + * Approved a majority of PRs (42 pull requests) + * Facilitated and encouraged discussion during meetings. + * Ensure that project deadlines are met timely. + +* **Enhancements implemented:** + * Wrote test cases for all note features/commands to increase coverage by ***+2.38%***, from ***65.96%*** to ***68.34%*** + * Pull requests [#100](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/100), Code coverage report [here](https://app.codecov.io/gh/AY2223S1-CS2103T-W12-2/tp/commit/2591db4951ad72aca890421c00da739c76e687ee) + * **Justification**: After adding 5 new commands for `Note` and implementation of `Note` itself, code coverage fell as many new classes were created. + * **Highlights**: Since notes are a completely new implementation, tedious work was required to set up `TypicalNotes`, an example of a typical note used for testing with. Extra utility classes were also needed to be created for the new notes. + +* **Contributions to team-based tasks:** + * Necessary implementations of entire Note features. + * Structure of Notes, its integration and all of its related commands. + * Updated parts of UG/DG not specific to a feature. + * Fixed bugs(including UG bugs) related to implementation of notes. + +* **Review/mentoring contributions:** + * Reviewed contributions/pull requests by other group members + * Reading and checking of pull requests by other members, subsequently approving them + * Approved a majority of PRs (42 pull requests approved total) + * Gave comments and helped correct bugs brought up in PRs + * Wrote the probable source of bug + * Directed to exact location of bug in a comment + * Example of such cases [here](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/75#pullrequestreview-1158333153) + +* **Contributions beyond the project team:** + * During PE-D, bugs were thoroughly sieved and detected. Feedbacks regarding bugs found were organised neatly into `small description of feature` and `problem found`. Feedbacks were accompanied by screenshots for better clarity and documenting. + * Suggestions were also made to strive and improve quality of product. + * [Links to section](https://github.com/Pinran-J/ped/issues) + +------------------- + +## Contributions to the UG: +1. Added section on `Introduction to SectresBook` (***not specific to feature***) + 1. [Link to section](https://ay2223s1-cs2103t-w12-2.github.io/tp/UserGuide.html#introduction-to-sectresbook) +2. Added section on `Using this guide` (***not specific to feature***) + 1. [Link to section](https://ay2223s1-cs2103t-w12-2.github.io/tp/UserGuide.html#using-this-guide) +3. Added sections on `Note Features` + 1. Including the following sections [(All of which can be found here)](https://ay2223s1-cs2103t-w12-2.github.io/tp/UserGuide.html#note-features) + 1. `Adding Notes: addNote` + 2. `Editing Notes: editNote` + 3. `Deleting Notes: deleteNote` + 4. `Locating a note by title: findNote` + 5. `Listing Notes: listNote` +4. Added some questions under `FAQ` + 1. [Link to section](https://ay2223s1-cs2103t-w12-2.github.io/tp/UserGuide.html#faq) + + +## Contributions to the DG: +1. **Helped implemented Model class diagram** + 1. ![Model class diagram](../images/ModelClassDiagram.png) + + +2. **Added entire section on `Notes Features`** + 1. Added section on `addNote feature` + 1. Includes : implementation, example scenario and design considerations + 2. Includes the following sequence diagram (using PlantUML) + 1. ![AddNoteSequenceDiagram](../images/AddNoteSequenceDiagram.png) + + 2. Added section on `deleteNote feature` + 1. Includes : implementation, example scenario and design considerations + 2. Includes the following sequence diagram (using PlantUML) + 1. ![DeleteNoteSequenceDiagram](../images/DeleteNoteSequenceDiagram.png) + + 3. Added section on `editNote feature` + 1. Includes : implementation, example scenario and design considerations + 2. Includes the following sequence diagram (using PlantUML) + 1. ![EditNoteSequenceDiagram](../images/EditNoteSequenceDiagram.png) + 3. Includes the following activity diagram (using PlantUML) + 1. ![EditNoteActivityDiagram](../images/EditNoteActivityDiagram.png) +3. **Added User stories** + 1. Added all user stories in the table. + +> Link to DG can be found [here](https://ay2223s1-cs2103t-w12-2.github.io/tp/DeveloperGuide.html) + +
diff --git a/docs/team/rui-han-crh.md b/docs/team/rui-han-crh.md new file mode 100644 index 00000000000..c1552845d98 --- /dev/null +++ b/docs/team/rui-han-crh.md @@ -0,0 +1,96 @@ +--- +layout: page +title: Chen Ruihan's Project Portfolio Page +--- + +### Project: SectresBook + +SectresBook helps secretaries to maintain all the information of the members of their club by collating a list of identifiable information, past records and future tasks.
Given below are my contributions to the project. + +**Code contributed**: [RepoSense link](https://nus-cs2103-ay2223s1.github.io/tp-dashboard/?search=w12&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-09-16&tabOpen=true&tabType=authorship&tabAuthor=rui-han-crh&tabRepo=AY2223S1-CS2103T-W12-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +**New Features Implemented** + +1. **_Loan Property of a Person_** ([#50](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/50)) + * **What it does**: Implements the ability to track monetary amounts that are represented as loaned amounts. The loan amounts must be between -1 trillion to 1 trillion inclusive. + * **Justification**: A treasurer requires the need to keep track of monetary transactions. A loan is a data type created to serve this purpose. + +2. **_User Interface Design_** ([#75](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/75)) + * **What it does**: Remodel and redesigned the user interface as shown in the landing page. Various small icons, images, alignment details and transitions are also applied for visual enhancement. + * **Justification**: The previous UI design did not look appealing, so a more visually appealing design was created. This will help the product's publicity and attractiveness to clients. + * **Highlights**: Event based triggers (space to start typing, esc to exit typing), transition effects and embellishments. + +3. **_Inspect command_** ([#75](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/75)) + * **What it does**: Inspects a person in the person's list. Inspection is a UI-centric command that updates UI values. It does not mutate any data in the model. This is also equivalent to just click on the person card. + * **Justification**: The UI requires more flexibility when coupled with the CLI, there shouldn't be things that the GUI can do that the CLI cannot, so the `inspect` command was created. + +4. **_Show and Hide Notes Panel Command_** ([#75](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/75)) + * **What it does**: Shows/hides the notes panel by applying a translational transition with a fade transition to the StackPane containing the notes panel with anchor points on an AnchorPane. + * **Justification**: It is difficult to view more than 6 people in the list at the same time especially if working on a monitor with a smaller resolution. By hiding the notes panel, more screen real estate can be given to the person list and inspect panel. + +5. **_Edit By Name_** ([#52](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/52)) + * **What it does**: Adding onto the ability to edit by index, I implemented an ability to edit by any keyword of the person's name. This makes it more convenient to specify edit operations without checking for name + * **Justification**: It is easier to recall a person's name than to read the index from the list. + +**Project management**: + +* Called meetings to discuss plans. +* Created milestones and description of milestones and provided feedback on Github PRs and organised issues with tags. + +**Contributions to team-based tasks**: + +* Setting up the GitHub team org/repo +* Changed the product icon to the current version (ledger with pen) using GIMP ([#102](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/102)) +* Released version v1.3.1 for PE-D and fixed 16 bugs after PE-D ([#179](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/179)) + +**Review/mentoring contributions**: + +* Reviewed contributions by other group members, sometimes suggesting other alternatives for better maintainability. +* Gave comments on suggestions for bug fixes in the issues tab and published issues based on more bugs found in the issues sections. + +**Contributions beyond the project team**: + +* Frequently posted in the forums to help other on iP related issues, such as setting the background, smoke testing and basic feature development. +* During the PE-D, authored issues based on the structure of `Description`, `Steps to reproduce` and `Suggestion`, making the issues clear and reproducible + +**Contributions to the UG/DG**: + +- UG + 1. Added images and icons + 2. Added the User Interface section of the UG with pointers to which part corresponds to what function and usage. [Link to UI section](https://ay2223s1-cs2103t-w12-2.github.io/tp/UserGuide.html#user-interface) + 3. Added description of properties for both people and notes, found [here](https://ay2223s1-cs2103t-w12-2.github.io/tp/UserGuide.html#properties) and description of commands [`inspect`](https://ay2223s1-cs2103t-w12-2.github.io/tp/UserGuide.html#inspecting-a-person--inspect), [`hideNotes`](https://ay2223s1-cs2103t-w12-2.github.io/tp/UserGuide.html#hiding-notes-panel--hidenotes) and [`showNotes`](https://ay2223s1-cs2103t-w12-2.github.io/tp/UserGuide.html#showing-notes-panel--shownotes). + 4. Added [Glossary](https://ay2223s1-cs2103t-w12-2.github.io/tp/UserGuide.html#glossary) terms. +- DG + 1. Added Edit _Sequence Diagram_ involving either searching by index or by name and Inspect _Sequence Diagram_ for the `inspect` command ([#84](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/84)) + 2. Updated the UI _Class Diagram_ to reflect the current state of the UI organisation and the Person _Class Diagram_ by adding the loans property to the Person _Class Diagram_ + 3. Wrote the sections involving [UI design](https://ay2223s1-cs2103t-w12-2.github.io/tp/DeveloperGuide.html#ui-features) and the implementation details of [`inspect`](https://ay2223s1-cs2103t-w12-2.github.io/tp/DeveloperGuide.html#inspect-feature), and `showNotes` and `hideNotes`, found [here](https://ay2223s1-cs2103t-w12-2.github.io/tp/DeveloperGuide.html#showing-and-hiding-the-notes-panel-feature). ([#189](https://github.com/AY2223S1-CS2103T-W12-2/tp/pull/189)) + 4. Added UI _Activity Diagram_ that describes how a user would interact with the UI + +----------------------------- + +### Diagram Extracts for DG + +**Edit _Sequence Diagram_** + +![](../images/EditSequenceDiagram.png) + +**Inspect _Sequence Diagram_** + +![](../images/InspectSequenceDiagram.png) + +

 

+ +**UI _Class Diagram_** + +![](../images/UiClassDiagram.png) + +**Person _Class Diagram_** + +![](../images/PersonClassDiagram.png) + +**UI _Activity Diagram_** + +![](../images/UIActivityDiagram.png) + +Text extracts to UG and DG are linked in the text above. + diff --git a/docs/team/ryanczx.md b/docs/team/ryanczx.md new file mode 100644 index 00000000000..6bd90459914 --- /dev/null +++ b/docs/team/ryanczx.md @@ -0,0 +1,53 @@ +--- +layout: page +title: Ryan Chua's Project Portfolio Page +--- + +### Project: SectresBook + +SectresBook helps secretaries to maintain all the information of the members of their club by collating a list of identifiable information, past records and future tasks. + +Given below are my contributions to the project. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2223s1.github.io/tp-dashboard/?search=w12-2&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-09-16&tabOpen=true&tabType=authorship&zFR=false&tabAuthor=ryanczx&tabRepo=AY2223S1-CS2103T-W12-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Enhancements implemented**: + * Enhancement: Restructure `Tag` + * Restructure `Tag` such that SectresBook has a `UniqueTagMapping` that `Person` and `Note` references. This allows SectresBook to only require one `Tag` object per unique tag, instead of each `Person` and `Note` needing their own `Tag` objects. + * In addition, every `Tag` has a `Person` list containing references of `Person` objects that are "tagged" with the `Tag`. + * This would allow for future features to better make use of `Tag`. + * Feature: Locating persons and notes by tag : `findTag` + * What it does: Finds `Person`s and `Note`s in SectresBook that have the given tags. + * Justification: This feature improves the product because secretaries can give `Person`s and `Note`s tags and find them by those given tags when they are needed in the future. + * Enhancement: Added the `Tag` functionality to `Note` + * Updated `Note` to have a `Tag` set. + * Updated `Note` commands to work with `Tag` functionality. + * Added tests to ensure that `Tag` functionality works properly + +* **Contributions to the UG**: + * Added section for Locating persons and notes by tag : `findTag` + * Brief description and explanation + * Screenshot for example + * Did final polishing and checks for UG + * Language use + * Formatting + * Organization + * Links + * Conversion to final PDF + +* **Contributions to the DG**: + * Updated `Model` component UML class diagram to reflect the correct associations between `UniqueTagMapping`, `Person`, `Note` and `Tag`. + * Added section for Find Person by Tag feature + * Brief description, explanation and design considerations + * Sequence diagram + +* **Contributions to team-based tasks**: + * Setting up the correct Codecov and CI links at the start. + * Managed the release v1.3.trial for the trial JAR activity. + +* **Review/mentoring contributions**: + * Read through, checked and approved team PRs. + * Occasionally offered suggestions or fixes for typos. + +* **Contributions beyond the project team**: + * Gave comments and suggestions for the bugs I found during the PE-D activity. diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 4133aaa0151..b0d72cdc9c4 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -2,10 +2,12 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.Objects; import java.util.Optional; import java.util.logging.Logger; import javafx.application.Application; +import javafx.scene.text.Font; import javafx.stage.Stage; import seedu.address.commons.core.Config; import seedu.address.commons.core.LogsCenter; @@ -36,7 +38,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 0, true); + public static final Version VERSION = new Version(1, 3, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -51,6 +53,8 @@ public void init() throws Exception { logger.info("=============================[ Initializing AddressBook ]==========================="); super.init(); + loadFonts(); + AppParameters appParameters = AppParameters.parse(getParameters()); config = initConfig(appParameters.getConfigPath()); @@ -68,6 +72,38 @@ public void init() throws Exception { ui = new UiManager(logic); } + private void loadFonts() { + Font.loadFont( + Objects.requireNonNull(MainApp.class.getResource("/fonts/MinionPro-Bold.otf")).toExternalForm(), + 10 + ); + + Font.loadFont( + Objects.requireNonNull(MainApp.class.getResource("/fonts/MinionPro-Medium.otf")).toExternalForm(), + 10 + ); + + Font.loadFont( + Objects.requireNonNull(MainApp.class.getResource("/fonts/MinionPro-Semibold.otf")).toExternalForm(), + 10 + ); + + Font.loadFont( + Objects.requireNonNull(MainApp.class.getResource("/fonts/Bender.otf")).toExternalForm(), + 10 + ); + + Font.loadFonts( + Objects.requireNonNull(MainApp.class.getResource("/fonts/Bender-Bold.otf")).toExternalForm(), + 10 + ); + + Font.loadFonts( + Objects.requireNonNull(MainApp.class.getResource("/fonts/Bender-Light.otf")).toExternalForm(), + 10 + ); + } + /** * Returns a {@code ModelManager} with the data from {@code storage}'s address book and {@code userPrefs}.
* The data from the sample address book will be used instead if {@code storage}'s address book is not found, @@ -169,6 +205,7 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { public void start(Stage primaryStage) { logger.info("Starting AddressBook " + MainApp.VERSION); ui.start(primaryStage); + primaryStage.setTitle("SectresBook"); } @Override diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/seedu/address/commons/core/GuiSettings.java index ba33653be67..0e631ac4d6a 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/seedu/address/commons/core/GuiSettings.java @@ -10,8 +10,8 @@ */ public class GuiSettings implements Serializable { - private static final double DEFAULT_HEIGHT = 600; - private static final double DEFAULT_WIDTH = 740; + private static final double DEFAULT_HEIGHT = 640; + private static final double DEFAULT_WIDTH = 1024; private final double windowWidth; private final double windowHeight; diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e469..5c93d489409 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -1,13 +1,62 @@ package seedu.address.commons.core; +import seedu.address.logic.parser.CliSyntax; + /** * Container for user visible messages. */ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; + public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; + + public static final String MESSAGE_INVALID_AMBIGUOUS_TITLE = "There is more than 1 note with %s in their title!\n" + + "Please use a more unique specifier or use indices to edit."; + + public static final String MESSAGE_INVALID_TITLE = "There are no notes with %s in their titles in the list!"; + + public static final String MESSAGE_INVALID_NOTE_DISPLAYED_INDEX = "The note index provided is invalid"; + public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; + public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_NOTES_LISTED_OVERVIEW = "%1$d notes listed!"; + + public static final String MESSAGE_NUMBER_TOO_SHORT = "Number to check must be at least 2 digits"; + + public static final String MESSAGE_INVALID_KEYWORD = "Keyword is invalid,keyword cannot contain special characters"; + + public static final String MESSAGE_INVALID_AMBIGUOUS_NAME = "There is more than 1 person with %s in their name!\n" + + "Please use a more unique specifier or use indices to edit."; + + public static final String MESSAGE_INVALID_NAME = "No names matched the given keywords %s!"; + + public static final String MESSAGE_INVALID_NON_POSITIVE_INDEX = "You may not specify non-positive indices!"; + + public static final String AMOUNT_NOT_SPECIFIED = "No amount to was specified to edit the loan with.\n" + + "Please use " + CliSyntax.PREFIX_LOAN_AMOUNT + " to specify a change in loan amount!"; + + public static final String REASON_NOT_SPECIFIED = "A reason must be given to change loan amounts.\n" + + "Please use " + CliSyntax.PREFIX_LOAN_REASON + " to specify a reason to change the loan value!"; + + public static final String OUT_OF_BOUNDS = "The index given to be inspected must be within " + + "the bounds of the list!"; + + public static final String NOT_AN_INTEGER = "The index given was not an integer value"; + + public static final String TOTAL_LOAN_OUT_OF_BOUNDS = "Operation refused as the total loan amount" + + " will be out of bounds."; + + public static final String AMBIGUOUS_NAME_INSPECT_FIRST = "There was more than one person of that name found.\n" + + "Showing the first person matching the given name.\n" + + "Note that inspection works only on the list you are currently viewing.\n" + + "Perhaps you would like to perform a find operation with keywords using " + + "the find command to narrow your search?"; + + public static final String MESSAGE_INVALID_NAME_INSPECT = "No names matched the given keywords %s!\n" + + "Note that inspection works only on the list you are currently viewing.\n" + + "Perhaps you would like to list out all persons with the list command " + + "to widen your search?"; } diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..e168e1ed94a 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -38,6 +38,54 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { .anyMatch(preppedWord::equalsIgnoreCase); } + /** + * Returns true if the {@code sentence} contains the {@code word}. + * Ignores case and special characters(!,.""),but a full word match is required. + *
examples:
+     *       containsWordIgnoreCase("ABc def", "abc") == true
+     *       containsWordIgnoreCase("ABc! def", "abc") == true
+     *       containsWordIgnoreCase("ABc2 def", "abc2") == true
+     *       containsWordIgnoreCase("ABc2 def", "abc") == false
+     *       containsWordIgnoreCase("ABc def", "DEF") == true
+     *       containsWordIgnoreCase("ABc def", "AB") == false //not a full word match
+     *       
+ * @param sentence cannot be null + * @param word cannot be null, cannot be empty, must be a single word + */ + public static boolean containsWordIgnoreCaseIgnoreSpecial(String sentence, String word) { + requireNonNull(sentence); + requireNonNull(word); + + String preppedWord = word.trim(); + checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); + checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word"); + + String preppedSentence = sentence.replaceAll("[^a-zA-Z0-9]", " "); + String[] wordsInPreppedSentence = preppedSentence.split("\\s+"); + + return Arrays.stream(wordsInPreppedSentence) + .anyMatch(preppedWord::equalsIgnoreCase); + } + + /** + * Returns true if the {@code phone } contains the {@code numbers}. + * Full phone number match is not required. + *
examples:
+     *       containsNumbers("91839344", "91839344") == true
+     *       containsNumbers("3829", "82") == true
+     *       containsNumbers("123", "1") == false //only 1 digit in numbers
+     *       containsNumbers("123", "4") == false
+     *       
+ * @param phone cannot be null + * @param numbers cannot be null, cannot be empty, must be at least 2 digits long. + */ + public static boolean containsNumbers(String phone, String numbers) { + requireNonNull(phone); + requireNonNull(numbers); + + return phone.startsWith(numbers); + } + /** * 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..76a28f9ec2c 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -8,6 +8,7 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.note.Note; import seedu.address.model.person.Person; /** @@ -33,6 +34,9 @@ public interface Logic { /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of notes */ + ObservableList getFilteredNoteList(); + /** * 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 9d9c6d15bdc..2dafb6fc409 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -14,6 +14,7 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.note.Note; import seedu.address.model.person.Person; import seedu.address.storage.Storage; @@ -42,7 +43,7 @@ public CommandResult execute(String commandText) throws CommandException, ParseE logger.info("----------------[USER COMMAND][" + commandText + "]"); CommandResult commandResult; - Command command = addressBookParser.parseCommand(commandText); + Command command = addressBookParser.parseCommand(commandText, model); commandResult = command.execute(model); try { @@ -64,6 +65,11 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getFilteredNoteList() { + return model.getFilteredNoteList(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 71656d7c5c8..09c92e94184 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; 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_PHONE; @@ -24,14 +25,15 @@ public class AddCommand extends Command { + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_BIRTHDAY + "BIRTHDAY " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "98765432 " + PREFIX_EMAIL + "johnd@example.com " + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_BIRTHDAY + "28/10/2000 " + + PREFIX_TAG + "friends"; public static final String MESSAGE_SUCCESS = "New person added: %1$s"; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; @@ -54,6 +56,9 @@ public CommandResult execute(Model model) throws CommandException { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } + // Add person reference to tags + toAdd.getTags().forEach(tag -> tag.addPerson(toAdd)); + model.addPerson(toAdd); return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); } diff --git a/src/main/java/seedu/address/logic/commands/AddNoteCommand.java b/src/main/java/seedu/address/logic/commands/AddNoteCommand.java new file mode 100644 index 00000000000..df62b123c76 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddNoteCommand.java @@ -0,0 +1,61 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTES_CONTENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTES_TITLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.note.Note; + +/** + * Adds a note to the address book. + */ +public class AddNoteCommand extends Command { + + public static final String COMMAND_WORD = "addNote"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a note to the address book. " + + "Parameters: " + + PREFIX_NOTES_TITLE + "TITLE " + + PREFIX_NOTES_CONTENT + "CONTENT " + + "[" + PREFIX_TAG + "TAG]... " + + "Example: " + COMMAND_WORD + " " + + PREFIX_NOTES_TITLE + "Club meetup " + + PREFIX_NOTES_CONTENT + "3rd October 9pm, everybody. " + + PREFIX_TAG + "friends"; + + public static final String MESSAGE_SUCCESS = "New note added: %1$s"; + public static final String MESSAGE_DUPLICATE_NOTE = "This note already exists in the address book"; + + private final Note toAdd; + + /** + * Creates an AddNoteCommand to add the specified {@code Note} + */ + public AddNoteCommand(Note note) { + requireNonNull(note); + toAdd = note; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasNote(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_NOTE); + } + + model.addNote(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddNoteCommand // instanceof handles nulls + && toAdd.equals(((AddNoteCommand) other).toAdd)); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 92f900b7916..2fb92b223ae 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; +import java.util.Arrays; import java.util.Objects; /** @@ -9,41 +10,51 @@ */ public class CommandResult { - private final String feedbackToUser; + /** + * Enum describing the state of the UI to trigger actions + */ + public enum UiState { + ShowHelp, + Exit, + Inspect, + HideNotes, ShowNotes, None + } + + private final UiState state; - /** Help information should be shown to the user. */ - private final boolean showHelp; + private final String[] args; - /** The application should exit. */ - private final boolean exit; + private final String feedbackToUser; /** - * Constructs a {@code CommandResult} with the specified fields. + * Constructs a {@code CommandResult} with the no additional state. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser) { this.feedbackToUser = requireNonNull(feedbackToUser); - this.showHelp = showHelp; - this.exit = exit; + this.state = UiState.None; + this.args = new String[0]; } /** - * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, - * and other fields set to their default value. + * Constructs a {@code CommandResult} with the specified state. */ - public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); - } - - public String getFeedbackToUser() { - return feedbackToUser; + public CommandResult(String feedbackToUser, UiState state, String... args) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.state = state; + this.args = args; } - public boolean isShowHelp() { - return showHelp; + /** + * Constructs a {@code CommandResult} with the specified state. + */ + public CommandResult(String feedbackToUser, UiState state) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.state = state; + this.args = new String[0]; } - public boolean isExit() { - return exit; + public String getFeedbackToUser() { + return feedbackToUser; } @Override @@ -59,13 +70,26 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) - && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && getUiState() == otherCommandResult.getUiState() + && Arrays.equals(getArgs(), otherCommandResult.getArgs()); } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, getUiState(), Arrays.hashCode(getArgs())); } + public UiState getUiState() { + return state; + } + + public String[] getArgs() { + return args; + } + + @Override + public String toString() { + return "[Feedback: " + getFeedbackToUser() + "] [UI State: " + getUiState() + + "] [" + Arrays.toString(getArgs()) + "]"; + } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 02fd256acba..f166a51fe94 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -19,28 +19,37 @@ public class DeleteCommand extends Command { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" + + "Parameters: INDEX (must be a positive integer) NAME (must be valid)\n" + "Example: " + COMMAND_WORD + " 1"; public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - private final Index targetIndex; + private Index targetIndex; public DeleteCommand(Index targetIndex) { this.targetIndex = targetIndex; } + @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); List lastShownList = model.getFilteredPersonList(); - if (targetIndex.getZeroBased() >= lastShownList.size()) { throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deletePerson(personToDelete); + + // Remove personToDelete from its tags, and remove unused tags from UniqueTagMapping + personToDelete.getTags().forEach(tag -> { + tag.removePerson(personToDelete); + if (tag.isPersonListEmpty() && !model.notebookContainsTag(tag)) { + model.removeTag(tag); + } + }); + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); } diff --git a/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java new file mode 100644 index 00000000000..8a8b4659ea1 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java @@ -0,0 +1,63 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.note.Note; + +/** + * Deletes a note identified using it's displayed index from the command listNotes. + */ +public class DeleteNoteCommand extends Command { + + public static final String COMMAND_WORD = "deleteNote"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the note identified by the index number used in the displayed listNotes command.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_NOTE_SUCCESS = "Deleted Note: %1$s"; + + private final Index targetIndex; + + public DeleteNoteCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredNoteList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_NOTE_DISPLAYED_INDEX); + } + + Note noteToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteNote(noteToDelete); + + // Remove unused tags from UniqueTagMapping + noteToDelete.getTags().forEach(tag -> { + if (tag.isPersonListEmpty() && !model.notebookContainsTag(tag)) { + model.removeTag(tag); + } + }); + + return new CommandResult(String.format(MESSAGE_DELETE_NOTE_SUCCESS, noteToDelete)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteNoteCommand // instanceof handles nulls + && targetIndex.equals(((DeleteNoteCommand) other).targetIndex)); // state check + } + + +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 7e36114902f..55b0c4b2b2e 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; 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_PHONE; @@ -20,7 +21,10 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; +import seedu.address.model.person.Loan; +import seedu.address.model.person.LoanHistory; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -36,11 +40,12 @@ public class EditCommand extends Command { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " + "by the index number used in the displayed person list. " + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " + + "Parameters: INDEX (must be a positive integer) NAME (must be valid) " + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_BIRTHDAY + "BIRTHDAY] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " @@ -82,6 +87,18 @@ public CommandResult execute(Model model) throws CommandException { } model.setPerson(personToEdit, editedPerson); + + // Remove personToEdit from its tags, and remove unused tags from UniqueTagMapping + personToEdit.getTags().forEach(tag -> { + tag.removePerson(personToEdit); + if (tag.isPersonListEmpty() && !model.notebookContainsTag(tag)) { + model.removeTag(tag); + } + }); + + // Add editedPerson to its tags + editedPerson.getTags().forEach(tag -> tag.addPerson(editedPerson)); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); } @@ -97,9 +114,13 @@ 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()); + Birthday updatedBirthday = editPersonDescriptor.getBirthday().orElse(personToEdit.getBirthday()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + Loan updatedLoan = editPersonDescriptor.getLoan().orElse(personToEdit.getLoan()); + List updatedHistory = editPersonDescriptor.getHistory().orElse(personToEdit.getHistory()); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, + updatedBirthday, updatedTags, updatedLoan, updatedHistory); } @Override @@ -129,7 +150,10 @@ public static class EditPersonDescriptor { private Phone phone; private Email email; private Address address; + private Birthday birthday; private Set tags; + private Loan loan; + private List history; public EditPersonDescriptor() {} @@ -142,14 +166,16 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setPhone(toCopy.phone); setEmail(toCopy.email); setAddress(toCopy.address); + setBirthday(toCopy.birthday); setTags(toCopy.tags); + setLoan(toCopy.loan); } /** * 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, birthday, tags, loan); } public void setName(Name name) { @@ -184,6 +210,30 @@ public Optional
getAddress() { return Optional.ofNullable(address); } + public void setBirthday(Birthday birthday) { + this.birthday = birthday; + } + + public Optional getBirthday() { + return Optional.ofNullable(birthday); + } + + public void setLoan(Loan loan) { + this.loan = loan; + } + + public Optional getLoan() { + return Optional.ofNullable(loan); + } + + public void setHistory(List history) { + this.history = history; + } + + public Optional> getHistory() { + return Optional.ofNullable(history); + } + /** * Sets {@code tags} to this object's {@code tags}. * A defensive copy of {@code tags} is used internally. @@ -201,6 +251,7 @@ public Optional> getTags() { return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); } + @Override public boolean equals(Object other) { // short circuit if same object @@ -220,7 +271,9 @@ public boolean equals(Object other) { && getPhone().equals(e.getPhone()) && getEmail().equals(e.getEmail()) && getAddress().equals(e.getAddress()) - && getTags().equals(e.getTags()); + && getBirthday().equals(e.getBirthday()) + && getTags().equals(e.getTags()) + && getLoan().equals(e.getLoan()); } } } diff --git a/src/main/java/seedu/address/logic/commands/EditLoanCommand.java b/src/main/java/seedu/address/logic/commands/EditLoanCommand.java new file mode 100644 index 00000000000..0ea8fff0267 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditLoanCommand.java @@ -0,0 +1,195 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LOAN_AMOUNT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LOAN_REASON; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; +import seedu.address.model.person.Email; +import seedu.address.model.person.Loan; +import seedu.address.model.person.LoanHistory; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.exceptions.LoanOutOfBoundsException; +import seedu.address.model.tag.Tag; + +/** + * Edits the loan value of an existing person in the address book. + */ +public class EditLoanCommand extends Command { + + public static final String COMMAND_WORD = "editLoan"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the loan value of the person identified " + + "by the index number used in the displayed person list. " + + "The existing loan value will be added to the input value.\n" + + "Parameters: INDEX (can be a positive or negative integer) NAME (must be valid) " + + PREFIX_LOAN_AMOUNT + "AMOUNT " + + PREFIX_LOAN_REASON + "REASON\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_LOAN_AMOUNT + "20 " + + PREFIX_LOAN_REASON + "Buy logistics"; + + public static final String MESSAGE_EDIT_LOAN_SUCCESS = "Edited loan of person: %1$s"; + public static final String OUT_OF_BOUNDS_NOTIFICATION = + Messages.TOTAL_LOAN_OUT_OF_BOUNDS + "\n" + Loan.MESSAGE_CONSTRAINTS; + + private final Index index; + private final EditLoanDescriptor editLoanDescriptor; + + /** + * @param index of the person in the filtered person list to edit + * @param editLoanDescriptor details to edit the person with + */ + public EditLoanCommand(Index index, EditLoanDescriptor editLoanDescriptor) { + requireNonNull(index); + requireNonNull(editLoanDescriptor); + + this.index = index; + this.editLoanDescriptor = editLoanDescriptor; + } + + @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()); + Person editedPerson; + try { + editedPerson = createEditedPerson(personToEdit, editLoanDescriptor); + } catch (LoanOutOfBoundsException e) { + throw new CommandException(OUT_OF_BOUNDS_NOTIFICATION); + } + + model.setPerson(personToEdit, editedPerson); + + // Remove personToEdit from its tags, and remove unused tags from UniqueTagMapping + personToEdit.getTags().forEach(tag -> { + tag.removePerson(personToEdit); + if (tag.isPersonListEmpty() && !model.notebookContainsTag(tag)) { + model.removeTag(tag); + } + }); + + // Add editedPerson to its tags + editedPerson.getTags().forEach(tag -> tag.addPerson(editedPerson)); + + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + int index = model.getFilteredPersonList().indexOf(editedPerson); + + return new CommandResult(String.format(MESSAGE_EDIT_LOAN_SUCCESS, editedPerson), + CommandResult.UiState.Inspect, String.format("%d", index + 1)); + } + + + private static Person createEditedPerson(Person personToEdit, EditLoanDescriptor editLoanDescriptor) + throws LoanOutOfBoundsException { + assert personToEdit != null; + + Name updatedName = personToEdit.getName(); + Phone updatedPhone = personToEdit.getPhone(); + Email updatedEmail = personToEdit.getEmail(); + Address updatedAddress = personToEdit.getAddress(); + Birthday updatedBirthday = personToEdit.getBirthday(); + Set updatedTags = personToEdit.getTags(); + + Loan updatedLoan = personToEdit.getLoan().addBy(editLoanDescriptor.getLoan() + .orElse(new Loan(0))); + + List updatedLoanHistory = new ArrayList<>(personToEdit.getHistory()); + editLoanDescriptor.getHistory().ifPresent(updatedLoanHistory::add); + + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, + updatedBirthday, updatedTags, updatedLoan, updatedLoanHistory); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditLoanCommand)) { + return false; + } + + // state check + EditLoanCommand e = (EditLoanCommand) other; + return index.equals(e.index) + && editLoanDescriptor.equals(e.editLoanDescriptor); + } + + /** + * Stores the loan details to edit the person with + */ + public static class EditLoanDescriptor { + private final Loan loan; + private final LoanHistory history; + + /** + * Constructs a new EditLoanDescriptor + * @param loan the new total loan + * @param history the new history to carry, included the increment loan and the reason + */ + public EditLoanDescriptor(Loan loan, LoanHistory history) { + this.loan = loan; + this.history = history; + } + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditLoanDescriptor(EditLoanDescriptor toCopy) { + this.loan = toCopy.loan; + this.history = toCopy.history; + } + + public Optional getLoan() { + return Optional.ofNullable(loan); + } + + public Optional getHistory() { + return Optional.ofNullable(history); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditLoanDescriptor)) { + return false; + } + + // state check + EditLoanDescriptor e = (EditLoanDescriptor) other; + + return getLoan().equals(e.getLoan()) + && getHistory().equals((e.getHistory())); + } + } + +} diff --git a/src/main/java/seedu/address/logic/commands/EditNoteCommand.java b/src/main/java/seedu/address/logic/commands/EditNoteCommand.java new file mode 100644 index 00000000000..0d0f91eaddc --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditNoteCommand.java @@ -0,0 +1,207 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTES_CONTENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTES_TITLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.note.Content; +import seedu.address.model.note.Note; +import seedu.address.model.note.Title; +import seedu.address.model.tag.Tag; + +/** + * Edits the details of an existing note in the address book. + */ +public class EditNoteCommand extends Command { + + public static final String COMMAND_WORD = "editNote"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the Note identified " + + "by the index number or the title used in the displayed note list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INDEX (must be a positive integer) TITLE (matches by substring) " + + "[" + PREFIX_NOTES_TITLE + "TITLE] " + + "[" + PREFIX_NOTES_CONTENT + "CONTENT] " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_NOTES_CONTENT + "New Content"; + + public static final String MESSAGE_EDIT_NOTE_SUCCESS = "Edited Note: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_NOTE = "This note already exists in the address book."; + + private final Index index; + private final EditNoteDescriptor editNoteDescriptor; + + + /** + * @param index of the note in the filtered note list to edit + * @param editNoteDescriptor details to edit the note with + */ + public EditNoteCommand(Index index, EditNoteDescriptor editNoteDescriptor) { + requireNonNull(index); + requireNonNull(editNoteDescriptor); + + this.index = index; + this.editNoteDescriptor = new EditNoteDescriptor(editNoteDescriptor); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredNoteList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_NOTE_DISPLAYED_INDEX); + } + + Note noteToEdit = lastShownList.get(index.getZeroBased()); + Note editedNote = createEditedNote(noteToEdit, editNoteDescriptor); + + if (!noteToEdit.isSameNote(editedNote) && model.hasNote(editedNote)) { + throw new CommandException(MESSAGE_DUPLICATE_NOTE); + } + + model.setNote(noteToEdit, editedNote); + + // Remove unused tags from UniqueTagMapping + noteToEdit.getTags().forEach(tag -> { + if (tag.isPersonListEmpty() && !model.notebookContainsTag(tag)) { + model.removeTag(tag); + } + }); + + model.updateFilteredNoteList(Model.PREDICATE_SHOW_ALL_NOTES); + return new CommandResult(String.format(MESSAGE_EDIT_NOTE_SUCCESS, editedNote)); + + } + + /** + * Creates and returns a {@code Note} with the details of {@code noteToEdit} + * edited with {@code editNoteDescriptor}. + */ + private static Note createEditedNote(Note noteToEdit, EditNoteDescriptor editNoteDescriptor) { + assert noteToEdit != null; + + Title updatedTitle = editNoteDescriptor.getTitle().orElse(noteToEdit.getTitle()); + Content updatedContent = editNoteDescriptor.getContent().orElse(noteToEdit.getContent()); + Set updatedTags = editNoteDescriptor.getTags().orElse(noteToEdit.getTags()); + + return new Note(updatedTitle, updatedContent, updatedTags); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditNoteCommand)) { + return false; + } + + // state check + EditNoteCommand e = (EditNoteCommand) other; + return index.equals(e.index) + && editNoteDescriptor.equals(e.editNoteDescriptor); + } + + + /** + * Stores the details to edit the note with. Each non-empty field value will replace the + * corresponding field value of the note. + */ + public static class EditNoteDescriptor { + private Title title; + private Content content; + private Set tags; + + public EditNoteDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is to be used internally. + */ + public EditNoteDescriptor(EditNoteDescriptor toCopy) { + setTitle(toCopy.title); + setContent(toCopy.content); + setTags(toCopy.tags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(title, content, tags); + } + + public void setTitle(Title title) { + this.title = title; + } + + public Optional getTitle() { + return Optional.ofNullable(title); + } + + public void setContent(Content content) { + this.content = content; + } + + public Optional<Content> getContent() { + return Optional.ofNullable(content); + } + + /** + * Sets {@code tags} to this object's {@code tags}. + * A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set<Tag> tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + + /** + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Optional<Set<Tag>> getTags() { + return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditNoteDescriptor)) { + return false; + } + + // state check + EditNoteDescriptor e = (EditNoteDescriptor) other; + + return getTitle().equals(e.getTitle()) + && getContent().equals(e.getContent()) + && getTags().equals(e.getTags()); + } + + } + +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..31999c2bc8d 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -13,7 +13,7 @@ public class ExitCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, CommandResult.UiState.Exit); } } diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index d6b19b0a0de..ca0ae5c64f1 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -15,7 +15,8 @@ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + + "the specified keywords (not case-sensitive) or phone number that starts with any of the given keywords" + + "(in digits) and then displays them as a list with index numbers.\n" + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + "Example: " + COMMAND_WORD + " alice bob charlie"; diff --git a/src/main/java/seedu/address/logic/commands/FindNoteCommand.java b/src/main/java/seedu/address/logic/commands/FindNoteCommand.java new file mode 100644 index 00000000000..75c0487dff8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindNoteCommand.java @@ -0,0 +1,45 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.model.Model; +import seedu.address.model.note.TitleContainsKeywordsPredicate; + +/** + * Finds and lists all notes in address book whose title contains any of the argument keywords. + * Keyword matching is case insensitive and ignores special characters in the title. + * Keyword must not be only special characters. Numbers are allowed. + */ +public class FindNoteCommand extends Command { + + public static final String COMMAND_WORD = "findNote"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all notes whose title contain any of " + + "the specified keywords (case-insensitive, ignores special characters) " + + "and displays them as a list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " club meeting"; + + private final TitleContainsKeywordsPredicate predicate; + + public FindNoteCommand(TitleContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredNoteList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_NOTES_LISTED_OVERVIEW, model.getFilteredNoteList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindNoteCommand // instanceof handles nulls + && predicate.equals(((FindNoteCommand) other).predicate)); // state check + } + +} diff --git a/src/main/java/seedu/address/logic/commands/FindTagCommand.java b/src/main/java/seedu/address/logic/commands/FindTagCommand.java new file mode 100644 index 00000000000..002d7ac09db --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindTagCommand.java @@ -0,0 +1,55 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.model.Model; +import seedu.address.model.note.NoteTagsContainsKeywordsPredicate; +import seedu.address.model.person.PersonTagsContainsKeywordsPredicate; + +/** + * Finds and lists all persons in address book who are tagged with any of the argument tags. + * Tag matching is case insensitive. + */ +public class FindTagCommand extends Command { + + public static final String COMMAND_WORD = "findTag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons and notes " + + "that are tagged with any of the specified tags (case-insensitive) and displays " + + "them as a person list and note list with index numbers.\n" + + "Parameters: TAG [MORE_TAGS]...\n" + + "Example: " + COMMAND_WORD + " Finance Tech"; + + private final PersonTagsContainsKeywordsPredicate personPredicate; + private final NoteTagsContainsKeywordsPredicate notePredicate; + + /** + * Constructs a FindTagCommand with the given {@code PersonTagsContainsKeywordsPredicate} + * and {@code NoteTagsContainsKeywordsPredicate} + */ + public FindTagCommand(PersonTagsContainsKeywordsPredicate personPredicate, + NoteTagsContainsKeywordsPredicate notePredicate) { + this.personPredicate = personPredicate; + this.notePredicate = notePredicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + + model.updateFilteredPersonList(personPredicate); + model.updateFilteredNoteList(notePredicate); + + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindTagCommand // instanceof handles nulls + && personPredicate.equals(((FindTagCommand) other).personPredicate) + && notePredicate.equals(((FindTagCommand) other).notePredicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..78ac4fcb260 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -16,6 +16,6 @@ public class HelpCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult(SHOWING_HELP_MESSAGE, CommandResult.UiState.ShowHelp); } } diff --git a/src/main/java/seedu/address/logic/commands/HideNotesPanelCommand.java b/src/main/java/seedu/address/logic/commands/HideNotesPanelCommand.java new file mode 100644 index 00000000000..852946dd119 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/HideNotesPanelCommand.java @@ -0,0 +1,21 @@ +package seedu.address.logic.commands; + +import seedu.address.model.Model; + +/** + * Format full help instructions for every command for display. + */ +public class HideNotesPanelCommand extends Command { + + public static final String COMMAND_WORD = "hideNotes"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Hides the notes panel.\n" + + "Example: " + COMMAND_WORD; + + public static final String HIDING_NOTES_PANEL_MESSAGE = "Hiding notes panel"; + + @Override + public CommandResult execute(Model model) { + return new CommandResult(HIDING_NOTES_PANEL_MESSAGE, CommandResult.UiState.HideNotes); + } +} diff --git a/src/main/java/seedu/address/logic/commands/InspectCommand.java b/src/main/java/seedu/address/logic/commands/InspectCommand.java new file mode 100644 index 00000000000..e6d503c4af8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/InspectCommand.java @@ -0,0 +1,41 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Deletes a person identified using it's displayed index from the address book. + */ +public class InspectCommand extends Command { + + public static final String COMMAND_WORD = "inspect"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Inspects the person identified by the index number used or by name registered" + + " in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer) <OR> NAME\n" + + "Example: " + COMMAND_WORD + " Alex"; + + public static final String MESSAGE_INSPECT_PERSON = "Inspecting Person"; + + private final String[] splitName; + + public InspectCommand(String[] splitName) { + this.splitName = splitName; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + return new CommandResult(MESSAGE_INSPECT_PERSON, CommandResult.UiState.Inspect, splitName); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof InspectCommand // instanceof handles nulls + && splitName.equals(((InspectCommand) other).splitName)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListNoteCommand.java b/src/main/java/seedu/address/logic/commands/ListNoteCommand.java new file mode 100644 index 00000000000..f93fa9520e3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ListNoteCommand.java @@ -0,0 +1,23 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_NOTES; + +import seedu.address.model.Model; + +/** + * Lists all the notes in address book to the user. + */ +public class ListNoteCommand extends Command { + + public static final String COMMAND_WORD = "listNote"; + + public static final String MESSAGE_SUCCESS = "Listed all notes"; + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredNoteList(PREDICATE_SHOW_ALL_NOTES); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ShowNotesPanelCommand.java b/src/main/java/seedu/address/logic/commands/ShowNotesPanelCommand.java new file mode 100644 index 00000000000..7ada2c150a8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ShowNotesPanelCommand.java @@ -0,0 +1,21 @@ +package seedu.address.logic.commands; + +import seedu.address.model.Model; + +/** + * Format full help instructions for every command for display. + */ +public class ShowNotesPanelCommand extends Command { + + public static final String COMMAND_WORD = "showNotes"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows the notes panel.\n" + + "Example: " + COMMAND_WORD; + + public static final String SHOWING_NOTES_PANEL_MESSAGE = "Showing notes panel"; + + @Override + public CommandResult execute(Model model) { + return new CommandResult(SHOWING_NOTES_PANEL_MESSAGE, CommandResult.UiState.ShowNotes); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 3b8bfa035e8..fec89a68201 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -2,18 +2,25 @@ import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; 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_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.stream.Stream; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; +import seedu.address.model.person.Loan; +import seedu.address.model.person.LoanHistory; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -24,6 +31,16 @@ */ public class AddCommandParser implements Parser<AddCommand> { + private final Model model; + + /** + * Constructs a {@code AddCommandParser} + * @param model the model of the current state + */ + public AddCommandParser(Model model) { + this.model = model; + } + /** * Parses the given {@code String} of arguments in the context of the AddCommand * and returns an AddCommand object for execution. @@ -31,9 +48,10 @@ public class AddCommandParser implements Parser<AddCommand> { */ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_BIRTHDAY, PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_BIRTHDAY, PREFIX_PHONE, PREFIX_EMAIL) || !argMultimap.getPreamble().isEmpty()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } @@ -42,9 +60,14 @@ public AddCommand parse(String args) throws ParseException { 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<Tag> tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + Birthday birthday = ParserUtil.parseBirthday(argMultimap.getValue(PREFIX_BIRTHDAY).get()); + Set<Tag> tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG), model); + Loan loan = new Loan(0); + + // construct empty loan history + List<LoanHistory> history = new ArrayList<>(); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, email, address, birthday, tagList, loan, history); return new AddCommand(person); } diff --git a/src/main/java/seedu/address/logic/parser/AddNoteCommandParser.java b/src/main/java/seedu/address/logic/parser/AddNoteCommandParser.java new file mode 100644 index 00000000000..b59b0d87093 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddNoteCommandParser.java @@ -0,0 +1,65 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTES_CONTENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTES_TITLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddNoteCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.note.Content; +import seedu.address.model.note.Note; +import seedu.address.model.note.Title; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new AddNoteCommand object + */ +public class AddNoteCommandParser { + + private final Model model; + + /** + * Constructs a {@code AddCommandParser} + * @param model the model of the current state + */ + public AddNoteCommandParser(Model model) { + this.model = model; + } + + /** + * Parses the given {@code String} of arguments in the context of the AddNoteCommand + * and returns an AddNoteCommand object for execution. + * + * @param args Argument to be parsed. + * @return AddNoteCommand to be executed. + * @throws ParseException if the user input does not conform the expected format. + */ + public AddNoteCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NOTES_TITLE, PREFIX_NOTES_CONTENT, PREFIX_TAG); + + if (!arePrefixesPresent(argMultimap, PREFIX_NOTES_TITLE, PREFIX_NOTES_CONTENT) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddNoteCommand.MESSAGE_USAGE)); + } + + Title title = ParserUtil.parseTitle(argMultimap.getValue(PREFIX_NOTES_TITLE).get()); + Content content = ParserUtil.parseContent(argMultimap.getValue(PREFIX_NOTES_CONTENT).get()); + Set<Tag> tagSet = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG), model); + + Note note = new Note(title, content, tagSet); + + return new AddNoteCommand(note); + } + + 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/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 1e466792b46..1154761f034 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -7,15 +7,26 @@ import java.util.regex.Pattern; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddNoteCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteNoteCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditLoanCommand; +import seedu.address.logic.commands.EditNoteCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindNoteCommand; +import seedu.address.logic.commands.FindTagCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.HideNotesPanelCommand; +import seedu.address.logic.commands.InspectCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ListNoteCommand; +import seedu.address.logic.commands.ShowNotesPanelCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; /** * Parses user input. @@ -34,7 +45,7 @@ public class AddressBookParser { * @return the command based on the user input * @throws ParseException if the user input does not conform the expected format */ - public Command parseCommand(String userInput) throws ParseException { + public Command parseCommand(String userInput, Model model) throws ParseException { final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); if (!matcher.matches()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); @@ -45,13 +56,13 @@ public Command parseCommand(String userInput) throws ParseException { switch (commandWord) { case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); + return new AddCommandParser(model).parse(arguments); case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); + return new EditCommandParser(model).parse(arguments); case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); + return new DeleteCommandParser(model).parse(arguments); case ClearCommand.COMMAND_WORD: return new ClearCommand(); @@ -59,6 +70,9 @@ public Command parseCommand(String userInput) throws ParseException { case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case FindTagCommand.COMMAND_WORD: + return new FindTagCommandParser().parse(arguments); + case ListCommand.COMMAND_WORD: return new ListCommand(); @@ -68,6 +82,33 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case InspectCommand.COMMAND_WORD: + return new InspectCommandParser().parse(arguments); + + case ShowNotesPanelCommand.COMMAND_WORD: + return new ShowNotesPanelCommand(); + + case HideNotesPanelCommand.COMMAND_WORD: + return new HideNotesPanelCommand(); + + case AddNoteCommand.COMMAND_WORD: + return new AddNoteCommandParser(model).parse(arguments); + + case ListNoteCommand.COMMAND_WORD: + return new ListNoteCommand(); + + case DeleteNoteCommand.COMMAND_WORD: + return new DeleteNoteCommandParser().parse(arguments); + + case FindNoteCommand.COMMAND_WORD: + return new FindNoteCommandParser().parse(arguments); + + case EditNoteCommand.COMMAND_WORD: + return new EditNoteCommandParser(model).parse(arguments); + + case EditLoanCommand.COMMAND_WORD: + return new EditLoanCommandParser(model).parse(arguments); + default: 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..5f67c7593ce 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -6,10 +6,14 @@ public class CliSyntax { /* Prefix definitions */ - public static final Prefix PREFIX_NAME = new Prefix("n/"); - 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_TAG = new Prefix("t/"); - + public static final Prefix PREFIX_NAME = new Prefix("name/"); + public static final Prefix PREFIX_PHONE = new Prefix("phone/"); + public static final Prefix PREFIX_EMAIL = new Prefix("email/"); + public static final Prefix PREFIX_ADDRESS = new Prefix("home/"); + public static final Prefix PREFIX_BIRTHDAY = new Prefix("bday/"); + public static final Prefix PREFIX_TAG = new Prefix("tag/"); + public static final Prefix PREFIX_NOTES_TITLE = new Prefix("title/"); + public static final Prefix PREFIX_NOTES_CONTENT = new Prefix("content/"); + public static final Prefix PREFIX_LOAN_AMOUNT = new Prefix("amt/"); + public static final Prefix PREFIX_LOAN_REASON = new Prefix("reason/"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 522b93081cc..7431242db03 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -1,15 +1,23 @@ package seedu.address.logic.parser; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; /** * Parses input arguments and creates a new DeleteCommand object */ public class DeleteCommandParser implements Parser<DeleteCommand> { + private Model model; + + /** + * Constructs a {@code DeleteCommandParser} + * @param model the model of the current state + */ + public DeleteCommandParser(Model model) { + this.model = model; + } /** * Parses the given {@code String} of arguments in the context of the DeleteCommand @@ -17,13 +25,15 @@ public class DeleteCommandParser implements Parser<DeleteCommand> { * @throws ParseException if the user input does not conform the expected format */ public DeleteCommand parse(String args) throws ParseException { + Index index; + String preamble = args.trim(); try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); + index = ParserUtil.parseIndex(preamble); } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + model.filterPersonListByName(preamble, DeleteCommand.MESSAGE_USAGE, pe); + index = Index.fromOneBased(1); } - } + return new DeleteCommand(index); + } } diff --git a/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java new file mode 100644 index 00000000000..76716ca8498 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java @@ -0,0 +1,35 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteNoteCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteNoteCommand object + */ +public class DeleteNoteCommandParser implements Parser<DeleteNoteCommand> { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteNoteCommand + * and returns a DeleteNoteCommand object for execution. + * + * @param args Argument to be parsed. + * @return DeleteNoteCommand to be executed. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteNoteCommand parse(String args) throws ParseException { + requireNonNull(args); + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteNoteCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteNoteCommand.MESSAGE_USAGE), pe); + + } + + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 845644b7dea..3fc3dba7607 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -1,8 +1,8 @@ package seedu.address.logic.parser; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; 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_PHONE; @@ -17,12 +17,22 @@ import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; import seedu.address.model.tag.Tag; /** * Parses input arguments and creates a new EditCommand object */ public class EditCommandParser implements Parser<EditCommand> { + private Model model; + + /** + * Constructs a {@code EditCommandParser} + * @param model the model of the current state + */ + public EditCommandParser(Model model) { + this.model = model; + } /** * Parses the given {@code String} of arguments in the context of the EditCommand @@ -32,14 +42,16 @@ public class EditCommandParser implements Parser<EditCommand> { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_BIRTHDAY, PREFIX_TAG); Index index; - + String preamble = argMultimap.getPreamble(); try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); + index = ParserUtil.parseIndex(preamble); } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); + model.filterPersonListByName(preamble, EditCommand.MESSAGE_USAGE, pe); + index = Index.fromOneBased(1); } EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -55,6 +67,9 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } + if (argMultimap.getValue(PREFIX_BIRTHDAY).isPresent()) { + editPersonDescriptor.setBirthday(ParserUtil.parseBirthday(argMultimap.getValue(PREFIX_BIRTHDAY).get())); + } parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { @@ -76,7 +91,7 @@ private Optional<Set<Tag>> parseTagsForEdit(Collection<String> tags) throws Pars return Optional.empty(); } Collection<String> tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); + return Optional.of(ParserUtil.parseTags(tagSet, model)); } } diff --git a/src/main/java/seedu/address/logic/parser/EditLoanCommandParser.java b/src/main/java/seedu/address/logic/parser/EditLoanCommandParser.java new file mode 100644 index 00000000000..a3be61e7976 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditLoanCommandParser.java @@ -0,0 +1,63 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LOAN_AMOUNT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LOAN_REASON; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditLoanCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.person.Loan; +import seedu.address.model.person.LoanHistory; +import seedu.address.model.person.Reason; + +/** + * Parses input arguments and creates a new EditLoanCommand object + */ +public class EditLoanCommandParser implements Parser<EditLoanCommand> { + + private Model model; + + /** + * Constructs a {@code EditLoanCommandParser} + * @param model the model of the current state + */ + public EditLoanCommandParser(Model model) { + this.model = model; + } + + /** + * Parses the given {@code String} of arguments in the context of the EditLoanCommand + * and returns an EditLoanCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditLoanCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_LOAN_AMOUNT, PREFIX_LOAN_REASON); + + Index index; + String preamble = argMultimap.getPreamble(); + try { + index = ParserUtil.parseIndex(preamble); + } catch (ParseException pe) { + model.filterPersonListByName(preamble, EditLoanCommand.MESSAGE_USAGE, pe); + index = Index.fromOneBased(1); + } + + Loan loanChange = ParserUtil.parseLoan(argMultimap.getValue(PREFIX_LOAN_AMOUNT) + .orElseThrow(() -> new ParseException(Messages.AMOUNT_NOT_SPECIFIED))); + + Reason reasonChange = ParserUtil.parseReason(argMultimap.getValue(PREFIX_LOAN_REASON) + .orElseThrow(() -> new ParseException(Messages.REASON_NOT_SPECIFIED))); + + LoanHistory toAdd = new LoanHistory(loanChange, reasonChange); + + EditLoanCommand.EditLoanDescriptor editLoanDescriptor = + new EditLoanCommand.EditLoanDescriptor(loanChange, toAdd); + + return new EditLoanCommand(index, editLoanDescriptor); + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditNoteCommandParser.java b/src/main/java/seedu/address/logic/parser/EditNoteCommandParser.java new file mode 100644 index 00000000000..779425e28d6 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditNoteCommandParser.java @@ -0,0 +1,121 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_AMBIGUOUS_TITLE; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_TITLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTES_CONTENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTES_TITLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import javafx.collections.ObservableList; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditNoteCommand; +import seedu.address.logic.commands.EditNoteCommand.EditNoteDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.note.Note; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new EditNoteCommand object + */ +public class EditNoteCommandParser implements Parser<EditNoteCommand> { + + private Model model; + + /** + * Constructs a {@code EditCommandParser} + * @param model the model of the current state + */ + public EditNoteCommandParser(Model model) { + this.model = model; + } + + /** + * Parses the given {@code String} of arguments in the context of the EditNoteCommand + * and returns an EditNoteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditNoteCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NOTES_TITLE, PREFIX_NOTES_CONTENT, PREFIX_TAG); + + String preamble = argMultimap.getPreamble(); + Index index; + + try { + index = ParserUtil.parseIndex(preamble); + } catch (ParseException pe) { + filterNotesByTitle(preamble, pe); + index = Index.fromOneBased(1); + } + + EditNoteDescriptor editNoteDescriptor = new EditNoteDescriptor(); + + if (argMultimap.getValue(PREFIX_NOTES_TITLE).isPresent()) { + editNoteDescriptor.setTitle(ParserUtil.parseTitle(argMultimap.getValue(PREFIX_NOTES_TITLE).get())); + } + if (argMultimap.getValue(PREFIX_NOTES_CONTENT).isPresent()) { + editNoteDescriptor.setContent(ParserUtil.parseContent(argMultimap.getValue(PREFIX_NOTES_CONTENT).get())); + } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editNoteDescriptor::setTags); + + if (!editNoteDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditNoteCommand.MESSAGE_NOT_EDITED); + } + + return new EditNoteCommand(index, editNoteDescriptor); + } + + /** + * Parses {@code Collection<String> tags} into a {@code Set<Tag>} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set<Tag>} containing zero tags. + */ + private Optional<Set<Tag>> parseTagsForEdit(Collection<String> tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection<String> tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet, model)); + } + + /** + * Filters the {@code ObservableList<Note>} by title + * @param preamble the name to search for, by complete word + * @param pe the ParseException to throw on failure + * @throws ParseException if there is nobody found by the find command, or there exist + * an ambiguity + */ + private void filterNotesByTitle(String preamble, ParseException pe) throws ParseException { + try { + new FindNoteCommandParser().parse(preamble).execute(model); + } catch (ParseException ignored) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditNoteCommand.MESSAGE_USAGE), pe); + } + + ObservableList<Note> filteredNoteList = model.getFilteredNoteList(); + + String splitPreamble = Arrays.stream(preamble.split(" ")) + .map(x -> "\"" + x.trim() + "\"") + .collect(Collectors.joining(" or ")); + + if (filteredNoteList.size() == 0) { + throw new ParseException(String.format(MESSAGE_INVALID_TITLE, splitPreamble), pe); + } else if (filteredNoteList.size() > 1) { + throw new ParseException(String.format(MESSAGE_INVALID_AMBIGUOUS_TITLE, splitPreamble), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 4fb71f23103..619d8e5da9d 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -1,6 +1,7 @@ package seedu.address.logic.parser; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_NUMBER_TOO_SHORT; import java.util.Arrays; @@ -26,7 +27,12 @@ public FindCommand parse(String args) throws ParseException { } String[] nameKeywords = trimmedArgs.split("\\s+"); - + for (String keyword : nameKeywords) { + if (keyword.matches("[0-9]+") && keyword.length() < 2) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_NUMBER_TOO_SHORT)); + } + } return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); } diff --git a/src/main/java/seedu/address/logic/parser/FindNoteCommandParser.java b/src/main/java/seedu/address/logic/parser/FindNoteCommandParser.java new file mode 100644 index 00000000000..28b6d5207f8 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindNoteCommandParser.java @@ -0,0 +1,47 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_KEYWORD; + +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.address.logic.commands.FindNoteCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.note.TitleContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FindNoteCommand object. + */ +public class FindNoteCommandParser implements Parser<FindNoteCommand> { + + /** + * Parses the given {@code String} of arguments in the context of the FindNoteCommand + * and returns a FindNoteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindNoteCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindNoteCommand.MESSAGE_USAGE)); + } + + String[] titleKeywords = trimmedArgs.split("\\s+"); + + Pattern p = Pattern.compile("[^a-zA-Z0-9]"); + + for (String keyword : titleKeywords) { + Matcher m = p.matcher(keyword); + if (m.find()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_INVALID_KEYWORD)); + + } + } + + return new FindNoteCommand(new TitleContainsKeywordsPredicate(Arrays.asList(titleKeywords))); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java b/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java new file mode 100644 index 00000000000..d6993d2b7b9 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java @@ -0,0 +1,35 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; + +import seedu.address.logic.commands.FindTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.note.NoteTagsContainsKeywordsPredicate; +import seedu.address.model.person.PersonTagsContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FindTagCommand object + */ +public class FindTagCommandParser implements Parser<FindTagCommand> { + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns a FindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindTagCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindTagCommand.MESSAGE_USAGE)); + } + + String[] tagKeywords = trimmedArgs.split("\\s+"); + + return new FindTagCommand(new PersonTagsContainsKeywordsPredicate(Arrays.asList(tagKeywords)), + new NoteTagsContainsKeywordsPredicate(Arrays.asList(tagKeywords))); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/InspectCommandParser.java b/src/main/java/seedu/address/logic/parser/InspectCommandParser.java new file mode 100644 index 00000000000..31e6e4d0368 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/InspectCommandParser.java @@ -0,0 +1,38 @@ +package seedu.address.logic.parser; + +import java.util.Arrays; + +import seedu.address.logic.commands.InspectCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Name; + +/** + * Parses input arguments and creates a new DeleteCommand object + */ +public class InspectCommandParser implements Parser<InspectCommand> { + + public static final String INSPECTION_FAILED_MESSAGE = "Inspection failed!\n"; + + /** + * Parses the given {@code String} of arguments in the context of the DeleteCommand + * and returns a DeleteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public InspectCommand parse(String args) throws ParseException { + String[] namePart = args.split(" ", 2); + + if (namePart.length < 2) { + return new InspectCommand(new String[0]); + } + + if (!Name.isValidName(namePart[1])) { + throw new ParseException(INSPECTION_FAILED_MESSAGE + Name.MESSAGE_CONSTRAINTS); + } + + String[] splitName = Arrays.stream(namePart[1].split(" ")) + .map(x -> x.trim().replaceAll(" +", " ")) + .filter(x -> !x.equals(" ")).toArray(String[]::new); + + return new InspectCommand(splitName); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..15c498c38db 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -4,15 +4,22 @@ import java.util.Collection; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.note.Content; +import seedu.address.model.note.Title; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; +import seedu.address.model.person.Loan; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.person.Reason; import seedu.address.model.tag.Tag; /** @@ -101,24 +108,108 @@ public static Email parseEmail(String email) throws ParseException { * * @throws ParseException if the given {@code tag} is invalid. */ - public static Tag parseTag(String tag) throws ParseException { + public static Tag parseTag(String tag, Model model) throws ParseException { requireNonNull(tag); String trimmedTag = tag.trim(); if (!Tag.isValidTagName(trimmedTag)) { throw new ParseException(Tag.MESSAGE_CONSTRAINTS); } - return new Tag(trimmedTag); + + //INSERTION POINT: Retrieve tag from unique list + //return new Tag(trimmedTag); + return Optional.ofNullable(model.getTagMapping().get(trimmedTag)).orElseGet( + // CHECKSTYLE.OFF: SeparatorWrap + () -> { + Tag newTag = new Tag(trimmedTag); + model.addTag(newTag); + return newTag; + }); } /** * Parses {@code Collection<String> tags} into a {@code Set<Tag>}. */ - public static Set<Tag> parseTags(Collection<String> tags) throws ParseException { + public static Set<Tag> parseTags(Collection<String> tags, Model model) throws ParseException { requireNonNull(tags); final Set<Tag> tagSet = new HashSet<>(); for (String tagName : tags) { - tagSet.add(parseTag(tagName)); + tagSet.add(parseTag(tagName, model)); } return tagSet; } + + /** + * Parses a {@code String title} into a {@code Title}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code title} is invalid. + */ + public static Title parseTitle(String title) throws ParseException { + requireNonNull(title); + String trimmedTitle = title.trim(); + if (!Title.isValidTitle(trimmedTitle)) { + throw new ParseException(Title.MESSAGE_CONSTRAINTS); + } + return new Title(trimmedTitle); + } + + /** + * Parses a {@code String content} into an {@code Content}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code content} is invalid. + */ + public static Content parseContent(String content) throws ParseException { + requireNonNull(content); + String trimmedContent = content.trim(); + if (!Content.isValidContent(trimmedContent)) { + throw new ParseException(Content.MESSAGE_CONSTRAINTS); + } + return new Content(trimmedContent); + } + + /** + * Parses a {@code String birthday} into a {@code Birthday}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code birthday} is invalid. + */ + public static Birthday parseBirthday(String birthday) throws ParseException { + requireNonNull(birthday); + String trimmedBirthday = birthday.trim(); + if (!Birthday.isValidBirthday(trimmedBirthday)) { + throw new ParseException(Birthday.MESSAGE_CONSTRAINTS); + } + return new Birthday(trimmedBirthday); + } + + /** + * Parses a {@code String loanAmout} into a {@code Loan}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code loanAmount} is invalid. + */ + public static Loan parseLoan(String loanAmount) throws ParseException { + requireNonNull(loanAmount); + String trimmedLoan = loanAmount.trim(); + if (!Loan.isValidLoan(trimmedLoan)) { + throw new ParseException(Loan.MESSAGE_CONSTRAINTS); + } + return new Loan(trimmedLoan); + } + + /** + * Parses a {@code String reason} into a {@code Reason}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code reason} is invalid. + */ + public static Reason parseReason(String reason) throws ParseException { + requireNonNull(reason); + String trimmedReason = reason.trim(); + if (!Reason.isValidReason(reason)) { + throw new ParseException(Reason.MESSAGE_CONSTRAINTS); + } + return new Reason(trimmedReason); + } } diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/seedu/address/logic/parser/Prefix.java index c859d5fa5db..a0b67f2b61d 100644 --- a/src/main/java/seedu/address/logic/parser/Prefix.java +++ b/src/main/java/seedu/address/logic/parser/Prefix.java @@ -2,7 +2,7 @@ /** * A prefix that marks the beginning of an argument in an arguments string. - * E.g. 't/' in 'add James t/ friend'. + * E.g. 'tag/' in 'add James tag/friend'. */ public class Prefix { private final String prefix; diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 1a943a0781a..11bcc6dbff1 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -3,10 +3,17 @@ import static java.util.Objects.requireNonNull; import java.util.List; +import java.util.Map; import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import seedu.address.model.note.Note; +import seedu.address.model.note.NoteBook; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; +import seedu.address.model.tag.Tag; +import seedu.address.model.tag.UniqueTagMapping; + /** * Wraps all data at the address-book level @@ -15,6 +22,9 @@ public class AddressBook implements ReadOnlyAddressBook { private final UniquePersonList persons; + private final NoteBook notebook; + private final UniqueTagMapping tags; + /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication @@ -25,6 +35,8 @@ public class AddressBook implements ReadOnlyAddressBook { */ { persons = new UniquePersonList(); + notebook = new NoteBook(); + tags = new UniqueTagMapping(); } public AddressBook() {} @@ -47,13 +59,61 @@ public void setPersons(List<Person> persons) { this.persons.setPersons(persons); } + public void setNotes(List<Note> notes) { + this.notebook.setNotes(notes); + } + + /** + * Replaces the contents of the person list with {@code persons}. + * {@code persons} must not contain duplicate persons. + */ + public void setTags(Map<String, Tag> tags) { + this.tags.setTags(tags); + } + /** * Resets the existing data of this {@code AddressBook} with {@code newData}. */ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); + setNotes(newData.getNoteBook()); setPersons(newData.getPersonList()); + setTags(newData.getTagMapping()); + } + + //// note-level operations + + /** + * Returns true if a note with the same identity as {@code note} exists in the address book. + * + * @param note The note the check with. + * @return True if address book has the specified note. + */ + public boolean hasNote(Note note) { + requireNonNull(note); + return notebook.contains(note); + } + + public void addNote(Note n) { + notebook.add(n); + } + + public void setNote(Note target, Note editedNote) { + requireNonNull(editedNote); + + notebook.setNote(target, editedNote); + } + + public void removeNote(Note key) { + notebook.remove(key); + } + + /** + * Returns true if at least one Note in the NoteBook contains the given tag. + */ + public boolean notebookContainsTag(Tag tag) { + return notebook.containsTag(tag); } //// person-level operations @@ -66,6 +126,7 @@ public boolean hasPerson(Person person) { return persons.contains(person); } + /** * Adds a person to the address book. * The person must not already exist in the address book. @@ -93,6 +154,16 @@ public void removePerson(Person key) { persons.remove(key); } + //// tag level operations + + public void addTag(Tag tag) { + tags.add(tag); + } + + public void removeTag(Tag tag) { + tags.remove(tag); + } + //// util methods @Override @@ -106,6 +177,16 @@ public ObservableList<Person> getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList<Note> getNoteBook() { + return notebook.asUnmodifiableObservableList(); + } + + @Override + public ObservableMap<String, Tag> getTagMapping() { + return tags.asUnmodifiableObservableMap(); + } + @Override public boolean equals(Object other) { return other == this // short circuit if same object diff --git a/src/main/java/seedu/address/model/DeepCopyable.java b/src/main/java/seedu/address/model/DeepCopyable.java new file mode 100644 index 00000000000..e39811e3b04 --- /dev/null +++ b/src/main/java/seedu/address/model/DeepCopyable.java @@ -0,0 +1,11 @@ +package seedu.address.model; + +/** + * Classes implementing {@code DeepCopyable} allows the object to recursively copy + * fields of its superclass and its own fields. + * Deep copying in this form is a form of prototyping and does not guarantee parent + * classes are deep copyable, unlike cloning, although they are suggested to be as well. + */ +public interface DeepCopyable { + Object deepCopy(); +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..c9c6b35f8cc 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -4,8 +4,12 @@ import java.util.function.Predicate; import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.note.Note; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * The API of the Model component. @@ -13,6 +17,7 @@ public interface Model { /** {@code Predicate} that always evaluate to true */ Predicate<Person> PREDICATE_SHOW_ALL_PERSONS = unused -> true; + Predicate<Note> PREDICATE_SHOW_ALL_NOTES = unused -> true; /** * Replaces user prefs data with the data in {@code userPrefs}. @@ -57,18 +62,25 @@ public interface Model { */ boolean hasPerson(Person person); + boolean hasNote(Note note); + /** * Deletes the given person. * The person must exist in the address book. */ void deletePerson(Person target); + void deleteNote(Note target); + /** * Adds the given person. * {@code person} must not already exist in the address book. */ void addPerson(Person person); + void addNote(Note target); + + /** * Replaces the given person {@code target} with {@code editedPerson}. * {@code target} must exist in the address book. @@ -76,12 +88,53 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); + void setNote(Note target, Note editedNote); + + /** + * Adds the given tag {@code tag}. + */ + void addTag(Tag tag); + + /** + * Deletes the given tag. + * The person tag exist in the address book. + */ + void removeTag(Tag tag); + + /** + * Returns an unmodifiable view of the address book's unique tag list. + */ + ObservableMap<String, Tag> getTagMapping(); + + /** + * Returns true if at least one Note in the NoteBook contains the given tag. + */ + boolean notebookContainsTag(Tag tag); + /** Returns an unmodifiable view of the filtered person list */ ObservableList<Person> getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered note list */ + ObservableList<Note> getFilteredNoteList(); + + /** + * Updates the filter of the filtered note list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredNoteList(Predicate<Note> predicate); + /** * 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<Person> predicate); + + /** + * Filters the {@code ObservableList<Person>} by person name + * @param preamble the name to search for, by complete word + * @param pe the ParseException to throw on failure + * @throws ParseException if there is nobody found by the find command, or there exist + * an ambiguity + */ + void filterPersonListByName(String preamble, String messageUsage, ParseException pe) throws ParseException; } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 86c1df298d7..fce9f5ca9c1 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -1,17 +1,29 @@ package seedu.address.model; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_AMBIGUOUS_NAME; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_NAME; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_NON_POSITIVE_INDEX; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Arrays; import java.util.function.Predicate; import java.util.logging.Logger; +import java.util.stream.Collectors; import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.parser.FindCommandParser; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.note.Note; +import seedu.address.model.person.Name; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * Represents the in-memory model of the address book data. @@ -22,6 +34,7 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList<Person> filteredPersons; + private final FilteredList<Note> filteredNotes; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -34,6 +47,7 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredNotes = new FilteredList<>(this.addressBook.getNoteBook()); } public ModelManager() { @@ -93,17 +107,34 @@ public boolean hasPerson(Person person) { return addressBook.hasPerson(person); } + @Override + public boolean hasNote(Note note) { + requireNonNull(note); + return addressBook.hasNote(note); + } + @Override public void deletePerson(Person target) { addressBook.removePerson(target); } + @Override + public void deleteNote(Note target) { + addressBook.removeNote(target); + } + @Override public void addPerson(Person person) { addressBook.addPerson(person); updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); } + @Override + public void addNote(Note note) { + addressBook.addNote(note); + } + + @Override public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); @@ -111,6 +142,33 @@ public void setPerson(Person target, Person editedPerson) { addressBook.setPerson(target, editedPerson); } + @Override + public void setNote(Note target, Note editedNote) { + requireAllNonNull(target, editedNote); + + addressBook.setNote(target, editedNote); + } + + @Override + public void addTag(Tag tag) { + addressBook.addTag(tag); + } + + @Override + public void removeTag(Tag tag) { + addressBook.removeTag(tag); + } + + @Override + public ObservableMap<String, Tag> getTagMapping() { + return addressBook.getTagMapping(); + } + + @Override + public boolean notebookContainsTag(Tag tag) { + return addressBook.notebookContainsTag(tag); + } + //=========== Filtered Person List Accessors ============================================================= /** @@ -128,6 +186,66 @@ public void updateFilteredPersonList(Predicate<Person> predicate) { filteredPersons.setPredicate(predicate); } + //=========== Filtered Note List Accessors ============================================================= + /** + * Returns an unmodifiable view of the list of {@code Note} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList<Note> getFilteredNoteList() { + return filteredNotes; + } + + @Override + public void updateFilteredNoteList(Predicate<Note> predicate) { + requireNonNull(predicate); + filteredNotes.setPredicate(predicate); + } + + /** + * Filters the {@code ObservableList<Person>} by person name, originating from + * some command + * @param preamble the name to search for, by complete word + * @param messageUsage the usage of the command this call originated from + * @param pe the ParseException to throw on failure + * @throws ParseException if there is nobody found by the find command, or there exist + * an ambiguity + */ + @Override + public void filterPersonListByName(String preamble, String messageUsage, + ParseException pe) throws ParseException { + try { + if (Integer.parseInt(preamble) <= 0) { + throw new ParseException(MESSAGE_INVALID_NON_POSITIVE_INDEX, pe); + } + } catch (NumberFormatException e) { + if (!Name.isValidName(preamble)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + messageUsage), pe); + } + } + + try { + new FindCommandParser().parse(preamble).execute(this); + } catch (ParseException ignored) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + messageUsage), pe); + } + + ObservableList<Person> filteredPersonList = getFilteredPersonList(); + + String splitPreamble = Arrays.stream(preamble.split(" ")) + .map(x -> "\"" + x.trim() + "\"") + .collect(Collectors.joining(" or ")); + + if (filteredPersonList.size() == 0) { + throw new ParseException(String.format(MESSAGE_INVALID_NAME, splitPreamble), pe); + } else if (filteredPersonList.size() > 1) { + throw new ParseException(String.format(MESSAGE_INVALID_AMBIGUOUS_NAME, splitPreamble), pe); + } + } + + @Override public boolean equals(Object obj) { // short circuit if same object diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..19b1dc2ef07 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,7 +1,10 @@ package seedu.address.model; import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import seedu.address.model.note.Note; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * Unmodifiable view of an address book @@ -13,5 +16,12 @@ public interface ReadOnlyAddressBook { * This list will not contain any duplicate persons. */ ObservableList<Person> getPersonList(); + ObservableList<Note> getNoteBook(); + + /** + * Returns an unmodifiable view of the tags list. + * This list will not contain any duplicate tags. + */ + ObservableMap<String, Tag> getTagMapping(); } diff --git a/src/main/java/seedu/address/model/ShallowCopyable.java b/src/main/java/seedu/address/model/ShallowCopyable.java new file mode 100644 index 00000000000..380036219c6 --- /dev/null +++ b/src/main/java/seedu/address/model/ShallowCopyable.java @@ -0,0 +1,14 @@ +package seedu.address.model; + +/** + * Classes implementing {@code ShallowCopyable} allows the object to copy only + * first-level fields of its own class. + * Shallow copying in this form is a form of prototyping and does not guarantee parent + * classes are shallow copyable, unlike cloning, although they are suggested to be as well. + * Shallow copy is used in place of clone if there are incompatibilities with the parent class + * or the fields consist of recursive substructures that could cause concurrency issues in + * multithreaded environments. + */ +public interface ShallowCopyable { + Object shallowCopy(); +} diff --git a/src/main/java/seedu/address/model/note/Content.java b/src/main/java/seedu/address/model/note/Content.java new file mode 100644 index 00000000000..007e8a00d7f --- /dev/null +++ b/src/main/java/seedu/address/model/note/Content.java @@ -0,0 +1,51 @@ +package seedu.address.model.note; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Note's content in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidContent(String)} + */ +public class Content { + + public static final String MESSAGE_CONSTRAINTS = + "Content should only contain alphanumeric characters, spaces, special characters, it should not be blank " + + "and no maximum length"; + + public static final String VALIDATION_REGEX = "[\\p{ASCII}][\\p{ASCII}]*"; + + public final String fullContent; + + /** + * Constructs a {@code Content}. + * + * @param content A valid content. + */ + public Content(String content) { + requireNonNull(content); + checkArgument(isValidContent(content), MESSAGE_CONSTRAINTS); + fullContent = content; + } + + public static boolean isValidContent(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return this.fullContent; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Content // instanceof handles nulls + && fullContent.equals(((Content) other).fullContent)); // state check + } + + @Override + public int hashCode() { + return fullContent.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/note/Note.java b/src/main/java/seedu/address/model/note/Note.java new file mode 100644 index 00000000000..9796a448db4 --- /dev/null +++ b/src/main/java/seedu/address/model/note/Note.java @@ -0,0 +1,112 @@ +package seedu.address.model.note; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import seedu.address.model.tag.Tag; + +/** + * Represents a note in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Note { + + //Identity field + private final Title title; + + //Data fields + private final Content content; + private final Set<Tag> tags = new HashSet<>(); + + /** + * Constructor for a Note object + * + * @param title Title of the note. + * @param content Contents of the note. + * @param tags Tags of the note. + */ + public Note(Title title, Content content, Set<Tag> tags) { + requireAllNonNull(title, content, tags); + this.title = title; + this.content = content; + this.tags.addAll(tags); + } + + public Title getTitle() { + return this.title; + } + + public Content getContent() { + return this.content; + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set<Tag> getTags() { + return Collections.unmodifiableSet(tags); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Note)) { + return false; + } + + Note otherNote = (Note) other; + return otherNote.getTitle().equals(getTitle()) + && otherNote.getContent().equals(getContent()) + && otherNote.getTags().equals(getTags()); + + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(title, content, tags); + } + + /** + * Returns true if both notes have the same title. + * This defines a weaker notion of equality between two notes. + * + * @param otherNote The other note to be compared to. + * @return True if both notes have same title. + */ + public boolean isSameNote(Note otherNote) { + if (otherNote == this) { + return true; + } + + return otherNote != null + && otherNote.getTitle().equals(getTitle()); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("Title: ") + .append(getTitle()) + .append(", Content: ") + .append(getContent()); + + Set<Tag> tags = getTags(); + if (!tags.isEmpty()) { + builder.append("; Tags: "); + tags.forEach(builder::append); + } + + return builder.toString(); + } + + +} diff --git a/src/main/java/seedu/address/model/note/NoteBook.java b/src/main/java/seedu/address/model/note/NoteBook.java new file mode 100644 index 00000000000..147883d076f --- /dev/null +++ b/src/main/java/seedu/address/model/note/NoteBook.java @@ -0,0 +1,133 @@ +package seedu.address.model.note; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.note.exceptions.DuplicateNoteException; +import seedu.address.model.note.exceptions.NoteNotFoundException; +import seedu.address.model.tag.Tag; + +/** + * A list of notes enforces uniqueness between its elements and does not allow nulls. + * A note is considered unique by comparing using {@code Note#isSameNote(Note)}. As such, adding and updating of + * note uses Note#isSameNote(Note) for equality so as to ensure that the note being added or updated is + * unique in terms of identity in the NoteBook. + * + * Supports a minimal set of list operations. + * + * @see Note#isSameNote(Note) + * + */ +public class NoteBook implements Iterable<Note> { + + private final ObservableList<Note> internalList = FXCollections.observableArrayList(); + private final ObservableList<Note> internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent note as the given argument. + */ + public boolean contains(Note toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameNote); + } + + /** + * Returns true if at least one Note in the NoteBook contains the given tag. + */ + public boolean containsTag(Tag tag) { + requireNonNull(tag); + return internalList.stream().anyMatch(note -> note.getTags().contains(tag)); + } + + /** + * Adds a note to the list. + * The note must not already exist in the list. + */ + public void add(Note toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateNoteException(); + } + internalList.add(toAdd); + } + + public void setNote(Note target, Note editedNote) { + requireAllNonNull(target, editedNote); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new NoteNotFoundException(); + } + + if (!target.isSameNote(editedNote) && contains(editedNote)) { + throw new DuplicateNoteException(); + } + + internalList.set(index, editedNote); + } + + /** + * Removes the equivalent note from the list. + * The note must exist in the list. + */ + public void remove(Note toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new NoteNotFoundException(); + } + } + + public ObservableList<Note> asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator<Note> iterator() { + return internalList.iterator(); + } + + public void setNotes(NoteBook replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + public void setNotes(List<Note> notes) { + requireAllNonNull(notes); + if (!notesAreUnique(notes)) { + throw new DuplicateNoteException(); + } + + internalList.setAll(notes); + + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof NoteBook // instanceof handles nulls + && internalList.equals(((NoteBook) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + private boolean notesAreUnique(List<Note> notes) { + for (int i = 0; i < notes.size() - 1; i++) { + for (int j = i + 1; j < notes.size(); j++) { + if (notes.get(i).isSameNote(notes.get(j))) { + return false; + } + } + } + return true; + } + +} diff --git a/src/main/java/seedu/address/model/note/NoteTagsContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/note/NoteTagsContainsKeywordsPredicate.java new file mode 100644 index 00000000000..eb94c50050e --- /dev/null +++ b/src/main/java/seedu/address/model/note/NoteTagsContainsKeywordsPredicate.java @@ -0,0 +1,40 @@ +package seedu.address.model.note; + +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.model.tag.Tag; + +/** + * Tests that a {@code Note}'s {@code Tags} matches any of the keywords given. + */ +public class NoteTagsContainsKeywordsPredicate implements Predicate<Note> { + private final List<String> keywords; + + public NoteTagsContainsKeywordsPredicate(List<String> keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Note note) { + boolean result = false; + + Set<Tag> tagSet = note.getTags(); + for (Tag tag : tagSet) { + result = result || keywords.stream().anyMatch( + keyword -> StringUtil.containsWordIgnoreCase(tag.tagName, keyword)); + } + + return result; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof NoteTagsContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((NoteTagsContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/note/Title.java b/src/main/java/seedu/address/model/note/Title.java new file mode 100644 index 00000000000..0af802e92a0 --- /dev/null +++ b/src/main/java/seedu/address/model/note/Title.java @@ -0,0 +1,53 @@ +package seedu.address.model.note; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Note's title in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidTitle(String)} + */ +public class Title { + + public static final String MESSAGE_CONSTRAINTS = + "Title should only contain alphanumeric characters, spaces, special characters, it should not be blank " + + "and maximum length 100"; + + public static final String VALIDATION_REGEX = "[\\p{ASCII}][\\p{ASCII}]{0,100}"; + //[A-Za-z0-9 _.,!'/$ ][A-Za-z0-9 _.,!/$} ]*"; + + public final String fullTitle; + + /** + * Constructs a {@code Title}. + * + * @param title A valid title. + */ + public Title(String title) { + requireNonNull(title); + checkArgument(isValidTitle(title), MESSAGE_CONSTRAINTS); + fullTitle = title; + } + + public static boolean isValidTitle(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return this.fullTitle; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Title // instanceof handles nulls + && fullTitle.equals(((Title) other).fullTitle)); // state check + } + + @Override + public int hashCode() { + return fullTitle.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/note/TitleContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/note/TitleContainsKeywordsPredicate.java new file mode 100644 index 00000000000..dea0b924236 --- /dev/null +++ b/src/main/java/seedu/address/model/note/TitleContainsKeywordsPredicate.java @@ -0,0 +1,31 @@ +package seedu.address.model.note; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Note}'s {@code Title} matches any of the keywords given. + */ +public class TitleContainsKeywordsPredicate implements Predicate<Note> { + private final List<String> keywords; + + public TitleContainsKeywordsPredicate(List<String> keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Note note) { + return keywords.stream().anyMatch( + keyword -> StringUtil.containsWordIgnoreCaseIgnoreSpecial(note.getTitle().fullTitle, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TitleContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((TitleContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/note/exceptions/DuplicateNoteException.java b/src/main/java/seedu/address/model/note/exceptions/DuplicateNoteException.java new file mode 100644 index 00000000000..2647eb21bca --- /dev/null +++ b/src/main/java/seedu/address/model/note/exceptions/DuplicateNoteException.java @@ -0,0 +1,11 @@ +package seedu.address.model.note.exceptions; + +/** + * Signals that the operation will result in duplicate Note (Notes are considered duplicates if they have the same + * title). + */ +public class DuplicateNoteException extends RuntimeException { + public DuplicateNoteException() { + super("Operation would result in duplicate note"); + } +} diff --git a/src/main/java/seedu/address/model/note/exceptions/NoteNotFoundException.java b/src/main/java/seedu/address/model/note/exceptions/NoteNotFoundException.java new file mode 100644 index 00000000000..d5301ab170c --- /dev/null +++ b/src/main/java/seedu/address/model/note/exceptions/NoteNotFoundException.java @@ -0,0 +1,8 @@ +package seedu.address.model.note.exceptions; + +/** + * Signals that the operation is unable to find the specified note. + */ +public class NoteNotFoundException extends RuntimeException { + +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 60472ca22a0..f08d4e44de0 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -3,11 +3,13 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import seedu.address.model.DeepCopyable; + /** * Represents a Person's address in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} */ -public class Address { +public class Address implements DeepCopyable { public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; @@ -54,4 +56,8 @@ public int hashCode() { return value.hashCode(); } + @Override + public Address deepCopy() { + return new Address(value); + } } diff --git a/src/main/java/seedu/address/model/person/Birthday.java b/src/main/java/seedu/address/model/person/Birthday.java new file mode 100644 index 00000000000..f2b0ed89383 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Birthday.java @@ -0,0 +1,62 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import seedu.address.model.DeepCopyable; + +/** + * Represents a Person's birthday in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidBirthday(String)} + */ +public class Birthday implements DeepCopyable { + + public static final String MESSAGE_CONSTRAINTS = "Birthdays must only take in valid months and days of " + + "the respective months in dd/MM/yyyy format"; + // solution adapted from https://stackoverflow.com/questions/15491894/regex-to-validate-date-formats-dd-mm-yyyy-dd- + // mm-yyyy-dd-mm-yyyy-dd-mmm-yyyy + public static final String DATE_REGEX = "(^(((0[1-9]|1[0-9]|2[0-8])[\\/](0[1-9]|1[012]))|((29|30|31)[\\/]" + + "(0[13578]|1[02]))|((29|30)[\\/](0[4,6,9]|11)))[\\/](19|[2-9][0-9])\\d\\d$)|(^29[\\/]02[\\/]" + + "(19|[2-9][0-9])(00|04|08|12|16|20|24|28|32|36|40|44|48|52|56|60|64|68|72|76|80|84|88|92|96)$)"; + public final String value; + + /** + * Constructs a {@code Birthday}. + * + * @param birthday A valid birthday. + */ + public Birthday(String birthday) { + requireNonNull(birthday); + checkArgument(isValidBirthday(birthday), MESSAGE_CONSTRAINTS); + value = birthday; + } + + /** + * Returns true if a given string is a valid Birthday. + */ + public static boolean isValidBirthday(String test) { + return test.matches(DATE_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Birthday // instanceof handles nulls + && value.equals(((Birthday) other).value)); // state check + } + + @Override + public Birthday deepCopy() { + return new Birthday(value); + } +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index f866e7133de..0e38aad3984 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -3,11 +3,13 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import seedu.address.model.DeepCopyable; + /** * Represents a Person's email in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} */ -public class Email { +public class Email implements DeepCopyable { private static final String SPECIAL_CHARACTERS = "+_.-"; public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " @@ -68,4 +70,9 @@ public int hashCode() { return value.hashCode(); } + @Override + public Email deepCopy() { + return new Email(value); + } + } diff --git a/src/main/java/seedu/address/model/person/Loan.java b/src/main/java/seedu/address/model/person/Loan.java new file mode 100644 index 00000000000..9606e4b3819 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Loan.java @@ -0,0 +1,166 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import seedu.address.model.DeepCopyable; +import seedu.address.model.person.exceptions.LoanOutOfBoundsException; + +/** + * Loan represents a class encapsulating an amount of money presently owed to the club. + */ +public class Loan implements DeepCopyable { + public static final String MESSAGE_CONSTRAINTS = + "Loan amounts should only contain numerics. Optionally, the decimal point (.), " + + "the dollar sign ($) and either plus or minus signs (+/-) may be used if desired.\n" + + "The sign, if used, must appear at the start of the number and before the dollar sign.\n" + + "The loan amount must be between negative 1 trillion and positive 1 trillion, both " + + "inclusive, and the precision may not exceed 2 decimal places."; + + + public static final String VALIDATION_REGEX = "^[-|+]?[$]?[0-9]\\d*(\\.\\d{0,2})?$"; + private static final double ONE_TRILLION = 1_000_000_000_000.00; + + private double amountOwed = 0; + + /** + * Constructs a {@code Loan}. + * + * @param amountString A valid amount, possibly in decimals + */ + public Loan(String amountString) { + requireNonNull(amountString); + checkArgument(isValidLoan(amountString), MESSAGE_CONSTRAINTS); + amountString = amountString.replace("$", ""); + + amountOwed = Double.parseDouble(amountString); + } + + /** + * Constructs a {@code Loan}. + * + * @param amount A double value to signifies a new loan amount + */ + public Loan(double amount) { + amount = (Math.round(amount * 100.0)) / 100.0; + checkArgument(isValidLoan(amount), MESSAGE_CONSTRAINTS); + amountOwed = amount; + } + + /** + * Checks if the given input satisfies the constraints + * @param test the input to test + * @return whether the constraint is satisfied + */ + public static boolean isValidLoan(String test) { + if (!test.matches(VALIDATION_REGEX)) { + return false; + } + test = test.replace("$", ""); + + double parsedAmount; + try { + parsedAmount = Double.parseDouble(test); + } catch (NumberFormatException e) { + return false; + } + return isValidLoan(parsedAmount); + } + + /** + * Checks if the given input satisfies the constraints + * @param test the input to test + * @return whether the constraint is satisfied + */ + public static boolean isValidLoan(double test) { + return Math.abs(test) <= ONE_TRILLION; + } + + + /** + * Subtracts the current loan with another loan and returns a new Loan object + * @param byLoan the amount to increase by + */ + public Loan subtractBy(Loan byLoan) throws LoanOutOfBoundsException { + try { + return new Loan(getAmount() - byLoan.getAmount()); + } catch (IllegalArgumentException e) { + throw new LoanOutOfBoundsException( + String.format("%f - %f will be out of bounds", getAmount(), byLoan.getAmount())); + } + } + + /** + * Adds the current loan with another loan and returns a new Loan object + * + * @param byLoan the amount to increase by + */ + public Loan addBy(Loan byLoan) throws LoanOutOfBoundsException { + try { + return new Loan(getAmount() + byLoan.getAmount()); + } catch (IllegalArgumentException e) { + throw new LoanOutOfBoundsException( + String.format("%f + %f will be out of bounds", getAmount(), byLoan.getAmount())); + } + } + + + public double getAmount() { + return amountOwed; + } + + @Override + public String toString() { + if (amountOwed < 0) { + return String.format("-$%.2f", -amountOwed); + } else { + return String.format("$%.2f", amountOwed); + } + } + + /** + * Returns a string version of the loan similar to a normal {@code toString()} call + * and appends the plus sign at the front for positive values + * @param withSign whether to print the sign + * @return a string version of the amount owed with sign if needed + */ + public String toString(boolean withSign) { + if (!withSign) { + return toString(); + } + + if (amountOwed < 0) { + return String.format("-$%.2f", -amountOwed); + } else { + return String.format("+$%.2f", amountOwed); + } + } + + /** + * Returns true if both loans have the same identity and data fields. + * This defines the notion of equality between two persons. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Loan)) { + return false; + } + + return ((Loan) other).amountOwed == amountOwed; + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Double.hashCode(amountOwed); + } + + @Override + public Loan deepCopy() { + return new Loan(amountOwed); + } +} diff --git a/src/main/java/seedu/address/model/person/LoanHistory.java b/src/main/java/seedu/address/model/person/LoanHistory.java new file mode 100644 index 00000000000..5d26f4da746 --- /dev/null +++ b/src/main/java/seedu/address/model/person/LoanHistory.java @@ -0,0 +1,74 @@ +package seedu.address.model.person; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +import seedu.address.model.ShallowCopyable; + +/** + * Represents a noteLoanHistory of a person in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class LoanHistory implements ShallowCopyable { + + private final Loan loanChange; + + private final Reason reason; + + /** + * Constructor for the LoanHistory object + * + * @param loanChange Change in loan amount. + * @param reason Reason for the change. + */ + public LoanHistory(Loan loanChange, Reason reason) { + requireAllNonNull(loanChange, reason); + this.loanChange = loanChange; + this.reason = reason; + } + + public Loan getLoanChange() { + return loanChange; + } + + public Reason getReason() { + return reason; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof LoanHistory)) { + return false; + } + + LoanHistory otherHistory = (LoanHistory) other; + return otherHistory.getLoanChange().equals(getLoanChange()) + && otherHistory.getReason().equals(getReason()); + } + + @Override + public int hashCode() { + return Objects.hash(loanChange, reason); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("LoanChange: ") + .append(getLoanChange()) + .append(", Reason: ") + .append(getReason()); + + return builder.toString(); + } + + @Override + public LoanHistory shallowCopy() { + return new LoanHistory(loanChange, reason); + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 79244d71cf7..a405a5832c4 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -3,20 +3,22 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import seedu.address.model.DeepCopyable; + /** * 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 DeepCopyable { public static final String MESSAGE_CONSTRAINTS = "Names should only contain alphanumeric characters and spaces, and it should not be blank"; /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. + * Disallows only numbers or Strings that contain numbers surrounded by whitespaces. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = + "^(?:[a-zA-Z]+|(?:[a-zA-Z0-9]*[a-zA-Z]\\d+[a-zA-Z0-9]*|[a-zA-Z0-9]*\\d+[a-zA-Z][a-zA-Z0-9]*)| )+$"; public final String fullName; @@ -56,4 +58,9 @@ public int hashCode() { return fullName.hashCode(); } + @Override + public Name deepCopy() { + return new Name(fullName); + } + } diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java index c9b5868427c..7bb7379b67e 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java @@ -18,7 +18,12 @@ public NameContainsKeywordsPredicate(List<String> keywords) { @Override public boolean test(Person person) { return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + .anyMatch(keyword -> (StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword) + || StringUtil.containsNumbers(person.getPhone().toString(), keyword))); + } + + public String getFirst() { + return keywords.get(0); } @Override diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index 8ff1d83fe89..f5a45364f18 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -2,38 +2,50 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; +import javafx.util.Pair; import seedu.address.model.tag.Tag; /** * Represents a Person in the address book. * Guarantees: details are present and not null, field values are validated, immutable. */ -public class Person { +public class Person implements seedu.address.model.DeepCopyable { // Identity fields private final Name name; private final Phone phone; private final Email email; + private final Birthday birthday; // Data fields private final Address address; private final Set<Tag> tags = new HashSet<>(); + private final Loan loan; + private final List<LoanHistory> history; /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set<Tag> tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Phone phone, Email email, Address address, Birthday birthday, Set<Tag> tags, + Loan loan, List<LoanHistory> history) { + requireAllNonNull(name, phone, email, address, tags, loan, birthday, history); this.name = name; this.phone = phone; this.email = email; this.address = address; + this.birthday = birthday; this.tags.addAll(tags); + this.loan = loan; + this.history = history; } public Name getName() { @@ -52,6 +64,9 @@ public Address getAddress() { return address; } + public Birthday getBirthday() { + return birthday; + } /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -60,6 +75,39 @@ public Set<Tag> getTags() { return Collections.unmodifiableSet(tags); } + public Loan getLoan() { + return loan; + } + + public List<LoanHistory> getHistory() { + return history; + } + + /** + * Combines the {@code LoanHistory} with {@code Loan} to produce a tracked history of loans together + * with the newly updated loan value at that point + * @return an {@code ArrayList} contains a {@code Pair} with key of type {@code Loan} and + * value of type {@code LoanHistory} + */ + public List<Pair<Loan, LoanHistory>> getHistoryWithTotal() { + Loan previousAmount = loan; + ArrayList<Pair<Loan, LoanHistory>> totalHistoryPair = new ArrayList<>(); + + ListIterator<LoanHistory> historyIterator = history.listIterator(history.size()); + + while (historyIterator.hasPrevious()) { + LoanHistory loanHistory = historyIterator.previous(); + totalHistoryPair.add(new Pair<>(previousAmount, loanHistory)); + + double nextPreviousLoan = previousAmount.getAmount() - loanHistory.getLoanChange().getAmount(); + nextPreviousLoan = Math.round(nextPreviousLoan * 100.0) / 100.0; + previousAmount = new Loan(nextPreviousLoan); + } + + return totalHistoryPair; + } + + /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. @@ -92,13 +140,15 @@ public boolean equals(Object other) { && otherPerson.getPhone().equals(getPhone()) && otherPerson.getEmail().equals(getEmail()) && otherPerson.getAddress().equals(getAddress()) - && otherPerson.getTags().equals(getTags()); + && otherPerson.getBirthday().equals(getBirthday()) + && otherPerson.getTags().equals(getTags()) + && otherPerson.getLoan().equals(getLoan()); } @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, birthday, tags, loan); } @Override @@ -110,7 +160,11 @@ public String toString() { .append("; Email: ") .append(getEmail()) .append("; Address: ") - .append(getAddress()); + .append(getAddress()) + .append("; Birthday: ") + .append((getBirthday())) + .append("; Owed amount: ") + .append(getLoan()); Set<Tag> tags = getTags(); if (!tags.isEmpty()) { @@ -120,4 +174,31 @@ public String toString() { return builder.toString(); } + /** + * Creates a new copy of this person object + * All fields are deep copied apart from Tag due to cyclical dependency. + * Tag clones contain shallow copies pointing to the Persons that the original + * tag contained. + * @return a new deeper-than-shallow copy of the Person's object + */ + @Override + public Person deepCopy() { + Person clonedPerson = new Person( + getName().deepCopy(), + getPhone().deepCopy(), + getEmail().deepCopy(), + getAddress().deepCopy(), + getBirthday().deepCopy(), + getTags().stream().map(Tag::shallowCopy).collect(Collectors.toSet()), + getLoan().deepCopy(), + getHistory().stream().map(LoanHistory::shallowCopy).collect(Collectors.toList())); + + clonedPerson.getTags().forEach(tag -> { + tag.removePerson(this); + tag.addPerson(clonedPerson); + }); + + return clonedPerson; + } + } diff --git a/src/main/java/seedu/address/model/person/PersonTagsContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/PersonTagsContainsKeywordsPredicate.java new file mode 100644 index 00000000000..143a6ce9369 --- /dev/null +++ b/src/main/java/seedu/address/model/person/PersonTagsContainsKeywordsPredicate.java @@ -0,0 +1,40 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.model.tag.Tag; + +/** + * Tests that a {@code Person}'s {@code Tags} matches any of the keywords given. + */ +public class PersonTagsContainsKeywordsPredicate implements Predicate<Person> { + private final List<String> keywords; + + public PersonTagsContainsKeywordsPredicate(List<String> keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + boolean result = false; + + Set<Tag> tagSet = person.getTags(); + for (Tag tag : tagSet) { + result = result || keywords.stream().anyMatch( + keyword -> StringUtil.containsWordIgnoreCase(tag.tagName, keyword)); + } + + return result; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PersonTagsContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((PersonTagsContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index 872c76b382f..c79147e15fb 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -50,4 +50,8 @@ public int hashCode() { return value.hashCode(); } + public Phone deepCopy() { + return new Phone(value); + } + } diff --git a/src/main/java/seedu/address/model/person/Reason.java b/src/main/java/seedu/address/model/person/Reason.java new file mode 100644 index 00000000000..61a43e6e63a --- /dev/null +++ b/src/main/java/seedu/address/model/person/Reason.java @@ -0,0 +1,50 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a reason for loan change of a person. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Reason { + + public static final String MESSAGE_CONSTRAINTS = + "The reason cannot be left blank!"; + + public static final String VALIDATION_REGEX = "[\\p{ASCII}][\\p{ASCII}]*"; + + public final String reason; + + /** + * Constructor for a Reason object + * + * @param reason Reason of the loan change. + */ + public Reason(String reason) { + requireNonNull(reason); + checkArgument(isValidReason(reason), MESSAGE_CONSTRAINTS); + this.reason = reason; + } + + public static boolean isValidReason(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return reason; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Reason // instanceof handles nulls + && reason.equals(((Reason) other).reason)); // state check + } + + @Override + public int hashCode() { + return reason.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/exceptions/LoanOutOfBoundsException.java b/src/main/java/seedu/address/model/person/exceptions/LoanOutOfBoundsException.java new file mode 100644 index 00000000000..c0889be3c39 --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/LoanOutOfBoundsException.java @@ -0,0 +1,12 @@ +package seedu.address.model.person.exceptions; + +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Signals that the operation is unable to find the specified person. + */ +public class LoanOutOfBoundsException extends ParseException { + public LoanOutOfBoundsException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index b0ea7e7dad7..1269982a06d 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -3,17 +3,28 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import seedu.address.model.ShallowCopyable; +import seedu.address.model.person.Person; + /** * Represents a Tag in the address book. * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} + * Tags are uniquely identified by their {@code tagName}. Two different tags may not share + * the same name. */ -public class Tag { +public class Tag implements ShallowCopyable { public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; public static final String VALIDATION_REGEX = "\\p{Alnum}+"; public final String tagName; + private final List<Person> personList = new ArrayList<>(); + /** * Constructs a {@code Tag}. * @@ -32,11 +43,71 @@ public static boolean isValidTagName(String test) { return test.matches(VALIDATION_REGEX); } + /** + * Adds person to tag's person list. + * @param person A person + */ + public void addPerson(Person person) { + if (!personList.contains(person)) { + this.personList.add(person); + } + } + + /** + * Removes person from tag's person list. + * @param person A person + */ + public void removePerson(Person person) { + this.personList.remove(person); + } + + /** + * Returns true if this tag's person list is empty. + */ + public boolean isPersonListEmpty() { + return personList.isEmpty(); + } + + /** + * Checks if a person exists under the tag group + * @param person the person to check for existence + * @return true if the person exists in this tag group + */ + public boolean doesPersonExist(Person person) { + return personList.contains(person); + } + + /** + * Creates a deep copy of the person's list of this tag object for read-only access + * Modifications to persons retrieved from this copied list will not affect the original + * person that it was copied from. + * Guarantees complete protection against mutations of persons through tag access + * @return a deep copy of the person's list + */ + public List<Person> getDeepCopiedPersonList() { + return personList.stream().map(Person::deepCopy).collect(Collectors.toList()); + } + + /** + * Returns true only if the names of two tags are the same. + * Two tags are defined to be equal only if their names are equal, it + * is not necessary for their persons contained within to be the same. + * @param other the other tag to check against + * @return true if two tags have the same tag name + */ @Override public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Tag // instanceof handles nulls - && tagName.equals(((Tag) other).tagName)); // state check + requireNonNull(other); + if (other == this) { + return true; + } + + if (!(other instanceof Tag)) { + return false; + } + + Tag otherTag = (Tag) other; + return otherTag.tagName.equals(tagName); } @Override @@ -47,8 +118,17 @@ public int hashCode() { /** * Format state as text for viewing. */ + @Override public String toString() { return '[' + tagName + ']'; } + @Override + public Tag shallowCopy() { + Tag clone = new Tag(tagName); + personList.forEach(clone::addPerson); + + return clone; + } + } diff --git a/src/main/java/seedu/address/model/tag/UniqueTagMapping.java b/src/main/java/seedu/address/model/tag/UniqueTagMapping.java new file mode 100644 index 00000000000..2f4187d45c0 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/UniqueTagMapping.java @@ -0,0 +1,129 @@ +package seedu.address.model.tag; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.Map; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableMap; +import seedu.address.model.tag.exceptions.DuplicateTagException; +import seedu.address.model.tag.exceptions.TagNotFoundException; + +/** + * A list of tags that enforces uniqueness between its elements and does not allow nulls. + * A tag is considered unique by comparing using {@code Tag#equal(Tag)}. As such, + * adding, updating and the removal of a tag uses {@code Tag#equals(Tag)} + * in order to ensure that the tag with exactly the same fields will be removed. + * {@code Tag#tagsAreUnique(tags)} will enforce a uniqueness check to guarantee that + * added Tags are unique, even if they are given by unique keys paired with non-unique tags. + * + * Supports a minimal set of list operations. + */ +public class UniqueTagMapping implements Iterable<Tag> { + + private final ObservableMap<String, Tag> internalMap = FXCollections.observableHashMap(); + private final ObservableMap<String, Tag> internalUnmodifiableMap = + FXCollections.unmodifiableObservableMap(internalMap); + + + /** + * Returns true if the list contains an equivalent tag as the given argument. + */ + public boolean contains(Tag toCheck) { + requireNonNull(toCheck); + return internalMap.values().stream().anyMatch(toCheck::equals); + } + + /** + * Returns true if the map contains the given tagName + * @param tagName the name of the tag to check against + * @return whether the mapping contains this tagName + */ + public boolean contains(String tagName) { + requireNonNull(tagName); + return internalMap.containsKey(tagName); + } + + /** + * Adds a tag to the list. + */ + public void add(Tag toAdd) { + requireNonNull(toAdd); + if (!contains(toAdd.tagName)) { + internalMap.put(toAdd.tagName, toAdd); + } + } + + /** + * Removes the equivalent tag from the list. + * The tag must exist in the list. + */ + public void remove(Tag toRemove) { + requireNonNull(toRemove); + if (!internalMap.containsKey(toRemove.tagName)) { + throw new TagNotFoundException(); + } + internalMap.remove(toRemove.tagName); + } + + /** + * Sets the internal mapping of this object to the same internal + * mapping of the replacement object, only if the tags are unique + * @param replacement the UniqueTagMapping to replace this object with + */ + public void setTags(UniqueTagMapping replacement) { + requireNonNull(replacement); + if (!tagsAreUnique(replacement.internalMap)) { + throw new DuplicateTagException(); + } + internalMap.clear(); + internalMap.putAll(replacement.internalMap); + } + + /** + * Replaces the contents of this list with {@code tags}. + * {@code tags} must not contain duplicate persons. + */ + public void setTags(Map<String, Tag> tags) { + requireAllNonNull(tags); + + if (!tagsAreUnique(tags)) { + throw new DuplicateTagException(); + } + internalMap.clear(); + internalMap.putAll(tags); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableMap<String, Tag> asUnmodifiableObservableMap() { + return internalUnmodifiableMap; + } + + @Override + public Iterator<Tag> iterator() { + return internalMap.values().iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueTagMapping // instanceof handles nulls + && internalMap.equals(((UniqueTagMapping) other).internalMap)); + } + + @Override + public int hashCode() { + return internalMap.hashCode(); + } + + /** + * Returns true if {@code tags} contains only unique tags. + */ + private boolean tagsAreUnique(Map<String, Tag> tags) { + return tags.values().stream().distinct().count() == tags.size(); + } +} diff --git a/src/main/java/seedu/address/model/tag/exceptions/DuplicateTagException.java b/src/main/java/seedu/address/model/tag/exceptions/DuplicateTagException.java new file mode 100644 index 00000000000..dc4c0a326c6 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/exceptions/DuplicateTagException.java @@ -0,0 +1,11 @@ +package seedu.address.model.tag.exceptions; + +/** + * Signals that the operation will result in duplicate Tags (Tags are considered duplicates if they have the same + * identity). + */ +public class DuplicateTagException extends RuntimeException { + public DuplicateTagException() { + super("Operation would result in duplicate tags"); + } +} diff --git a/src/main/java/seedu/address/model/tag/exceptions/TagNotFoundException.java b/src/main/java/seedu/address/model/tag/exceptions/TagNotFoundException.java new file mode 100644 index 00000000000..9de0aa2c897 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/exceptions/TagNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.tag.exceptions; + +/** + * Signals that the operation is unable to find the specified tag. + */ +public class TagNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..8b24e8f5224 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,16 +1,25 @@ package seedu.address.model.util; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.note.Content; +import seedu.address.model.note.Note; +import seedu.address.model.note.Title; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; +import seedu.address.model.person.Loan; +import seedu.address.model.person.LoanHistory; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Reason; import seedu.address.model.tag.Tag; /** @@ -19,32 +28,149 @@ public class SampleDataUtil { public static Person[] getSamplePersons() { return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + new Person( + new Name("Lynette Chong"), + new Phone("91883124"), + new Email("lynette@example.com"), + new Address("Blk 2 Holland Avenue, #23-05"), + new Birthday("09/07/1995"), + getTagSet("president"), + new Loan(0), List.of( + new LoanHistory(new Loan(100), new Reason("Deposited $100")), + new LoanHistory(new Loan(-100), new Reason("Invoice #066-011445-305")) + ) + ), + + new Person( + new Name("Gerald Yap"), + new Phone("84558922"), + new Email("gerald_yap@example.com"), + new Address("Blk 432 Bukit Panjang Ring Rd, #12-40"), + new Birthday("10/11/2000"), + getTagSet("colleagues", "operations"), + new Loan(-426), List.of( + new LoanHistory(new Loan(8), new Reason("Buy shirt")), + new LoanHistory(new Loan(-100), new Reason("Invoice #122-10493-293")), + new LoanHistory(new Loan(-200), new Reason("Invoice #133-589313-211")), + new LoanHistory(new Loan(-184), new Reason("Invoice #158-970377-008")), + new LoanHistory(new Loan(50), new Reason("Pay back excess charge")) + ) + ), + + new Person( + new Name("Alex Yeoh"), + new Phone("87438807"), + new Email("alexyeoh@example.com"), + new Address("Blk 30 Geylang Street 29, #06-40"), + new Birthday("20/01/2003"), + getTagSet("friends", "operations"), + new Loan(0), new ArrayList<LoanHistory>()), + + new Person( + new Name("Bernice Yu"), + new Phone("99272758"), + new Email("berniceyu@example.com"), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), + new Birthday("18/06/1999"), + getTagSet("colleagues", "friends"), + new Loan(48), List.of( + new LoanHistory(new Loan(8), new Reason("Buy shirt")), + new LoanHistory(new Loan(40), new Reason("Loaned money to buy 2x white paint")) + ) + ), + + new Person( + new Name("Charlotte Oliveiro"), + new Phone("93210283"), + new Email("charlotte@example.com"), + new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), + new Birthday("03/05/1976"), + getTagSet("operations"), + new Loan(0), List.of( + new LoanHistory(new Loan(8), new Reason("Buy shirt")), + new LoanHistory(new Loan(42), new Reason("Loaned money to buy foam boards")), + new LoanHistory(new Loan(-80), new Reason("Invoice #210-399032-029")), + new LoanHistory(new Loan(30), new Reason("Paid back loaned amount")) + ) + ), + + new Person( + new Name("David Li"), + new Phone("91031282"), + new Email("lidavid@example.com"), + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), + new Birthday("11/04/1985"), + getTagSet("family"), + new Loan(-42), List.of( + new LoanHistory(new Loan(8), new Reason("Buy shirt")), + new LoanHistory(new Loan(-50), new Reason("To pay back after fixing table")) + ) + ), + + new Person( + new Name("Irfan Ibrahim"), + new Phone("92492021"), + new Email("irfan@example.com"), + new Address("Blk 47 Tampines Street 20, #17-35"), + new Birthday("25/09/2010"), + getTagSet("classmates"), + new Loan(100), List.of( + new LoanHistory(new Loan(100), new Reason("Borrowed for mass dinner event funds")) + ) + ), + + new Person( + new Name("Roy Balakrishnan"), + new Phone("92624417"), + new Email("royb@example.com"), + new Address("Blk 45 Aljunied Street 85, #11-31"), + new Birthday("09/07/2005"), + getTagSet("colleagues"), + new Loan(0), new ArrayList<LoanHistory>()) }; } + public static Tag[] getSampleTags() { + return new Tag[] { + new Tag("friends"), + new Tag("colleagues"), + new Tag("operations"), + new Tag("family"), + new Tag("classmates") + }; + } + + public static Note[] getSampleNotes() { + return new Note[] { + new Note( + new Title("Indent buffet for meeting"), + new Content("Buffet to be indented at 3pm next Tuesday"), + getTagSet("colleagues") + ), + new Note( + new Title("Collect funds from operations team"), + new Content("Collect $20 from everyone involved in operations"), + getTagSet("operations") + ), + new Note( + new Title("Check meeting availability"), + new Content("Prepare attendance roll for those coming for next Tuesday's meeting"), + getTagSet("president") + ) + }; + } + public static ReadOnlyAddressBook getSampleAddressBook() { AddressBook sampleAb = new AddressBook(); for (Person samplePerson : getSamplePersons()) { sampleAb.addPerson(samplePerson); } + for (Tag sampleTag : getSampleTags()) { + sampleAb.addTag(sampleTag); + } + for (Note sampleNote : getSampleNotes()) { + sampleAb.addNote(sampleNote); + } return sampleAb; } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedLoanHistory.java b/src/main/java/seedu/address/storage/JsonAdaptedLoanHistory.java new file mode 100644 index 00000000000..38d7466ae34 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedLoanHistory.java @@ -0,0 +1,51 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Loan; +import seedu.address.model.person.LoanHistory; +import seedu.address.model.person.Reason; + +/** + * Jackson-friendly version of {@link LoanHistory}. + */ +public class JsonAdaptedLoanHistory { + + private final String loan; + private final String reason; + + /** + * Constructs a {@code JsonAdaptedLoanHistory} with the given details. + */ + @JsonCreator + public JsonAdaptedLoanHistory(@JsonProperty("loan") String loan, @JsonProperty("reason") String reason) { + this.loan = loan; + this.reason = reason; + } + + /** + * Converts a given {@code LoanHistory} into this class for Jackson use. + */ + public JsonAdaptedLoanHistory(LoanHistory source) { + this.loan = source.getLoanChange().toString(); + this.reason = source.getReason().toString(); + } + + /** + * Converts this Jackson-friendly adapted LoanHistory object into the model's {@code LoanHistory} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted LoanHistory. + */ + public LoanHistory toModelType() throws IllegalValueException { + if (!Loan.isValidLoan(loan)) { + throw new IllegalValueException(Loan.MESSAGE_CONSTRAINTS); + } + if (!Reason.isValidReason(reason)) { + throw new IllegalValueException(Reason.MESSAGE_CONSTRAINTS); + } + + return new LoanHistory(new Loan(loan), new Reason(reason)); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedNote.java b/src/main/java/seedu/address/storage/JsonAdaptedNote.java new file mode 100644 index 00000000000..1af0e419aec --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedNote.java @@ -0,0 +1,86 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.note.Content; +import seedu.address.model.note.Note; +import seedu.address.model.note.Title; +import seedu.address.model.tag.Tag; + +/** + * Jackson-friendly version of {@link Note}. + */ +public class JsonAdaptedNote { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Note's %s field is missing!"; + + private final String title; + private final String content; + private final List<JsonAdaptedTag> tagged = new ArrayList<>(); + + /** + * Constructs a {@code JsonAdaptedNote} with the given note details. + */ + @JsonCreator + public JsonAdaptedNote(@JsonProperty("title") String title, @JsonProperty("content") String content, + @JsonProperty("tagged") List<JsonAdaptedTag> tagged) { + this.title = title; + this.content = content; + if (tagged != null) { + this.tagged.addAll(tagged); + } + } + + /** + * Converts a given {@code Note} into this class for Jackson use. + */ + public JsonAdaptedNote(Note source) { + title = source.getTitle().fullTitle; + content = source.getContent().fullContent; + tagged.addAll(source.getTags().stream() + .map(JsonAdaptedTag::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted note object into the model's {@code Note} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted note. + */ + public Note toModelType(List<Tag> addressBookTagList) throws IllegalValueException { + final List<Tag> convertedTags = new ArrayList<>(); + for (JsonAdaptedTag adaptedTag : tagged) { + convertedTags.add(adaptedTag.toModelType()); + } + + final Set<Tag> modelTags = addressBookTagList.stream() + .filter(convertedTags::contains) + .collect(Collectors.toSet()); + + if (title == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Title.class.getSimpleName())); + } + if (!Title.isValidTitle(title)) { + throw new IllegalValueException(Title.MESSAGE_CONSTRAINTS); + } + final Title modelTitle = new Title(title); + + if (content == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Content.class.getSimpleName())); + } + if (!Content.isValidContent(content)) { + throw new IllegalValueException(Content.MESSAGE_CONSTRAINTS); + } + final Content modelContent = new Content(content); + + return new Note(modelTitle, modelContent, modelTags); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index a6321cec2ea..ff545f3a28f 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -1,7 +1,6 @@ package seedu.address.storage; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -11,7 +10,10 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; +import seedu.address.model.person.Loan; +import seedu.address.model.person.LoanHistory; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -28,7 +30,10 @@ class JsonAdaptedPerson { private final String phone; private final String email; private final String address; + private final String birthday; private final List<JsonAdaptedTag> tagged = new ArrayList<>(); + private final String loan; + private final List<JsonAdaptedLoanHistory> history; /** * Constructs a {@code JsonAdaptedPerson} with the given person details. @@ -36,14 +41,19 @@ class JsonAdaptedPerson { @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tagged") List<JsonAdaptedTag> tagged) { + @JsonProperty("birthday") String birthday, + @JsonProperty("tagged") List<JsonAdaptedTag> tagged, + @JsonProperty("loan") String loan, @JsonProperty("history") List<JsonAdaptedLoanHistory> history) { this.name = name; this.phone = phone; this.email = email; this.address = address; + this.birthday = birthday; if (tagged != null) { this.tagged.addAll(tagged); } + this.loan = loan; + this.history = history; } /** @@ -54,22 +64,39 @@ public JsonAdaptedPerson(Person source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; + birthday = source.getBirthday().value; tagged.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + loan = source.getLoan().toString(); + history = source.getHistory().stream().map(JsonAdaptedLoanHistory::new).collect(Collectors.toList()); + } + /** * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. - * + * @param addressBookTagList the list of tags that exist in the addressBook to be assigned to + * the model's {@code Person} object * @throws IllegalValueException if there were any data constraints violated in the adapted person. */ - public Person toModelType() throws IllegalValueException { - final List<Tag> personTags = new ArrayList<>(); - for (JsonAdaptedTag tag : tagged) { - personTags.add(tag.toModelType()); + public Person toModelType(List<Tag> addressBookTagList) throws IllegalValueException { + final List<Tag> convertedTags = new ArrayList<>(); + for (JsonAdaptedTag adaptedTag : tagged) { + convertedTags.add(adaptedTag.toModelType()); + } + + final Set<Tag> modelTags = addressBookTagList.stream() + .filter(convertedTags::contains) + .collect(Collectors.toSet()); + + final List<LoanHistory> modelHistory = new ArrayList<>(); + for (JsonAdaptedLoanHistory adaptedLoan : history) { + modelHistory.add(adaptedLoan.toModelType()); } + // We could really use some abstraction here -- Rui Han + // Name validity check if (name == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); } @@ -78,6 +105,7 @@ public Person toModelType() throws IllegalValueException { } final Name modelName = new Name(name); + // Phone validity check if (phone == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); } @@ -86,6 +114,7 @@ public Person toModelType() throws IllegalValueException { } final Phone modelPhone = new Phone(phone); + // Email validity check if (email == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); } @@ -94,6 +123,8 @@ public Person toModelType() throws IllegalValueException { } final Email modelEmail = new Email(email); + + // Address validity check if (address == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); } @@ -102,8 +133,27 @@ public Person toModelType() throws IllegalValueException { } final Address modelAddress = new Address(address); - final Set<Tag> modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + // Birthday validity check + if (birthday == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, Birthday.class.getSimpleName())); + } + if (!Birthday.isValidBirthday(birthday)) { + throw new IllegalValueException(Birthday.MESSAGE_CONSTRAINTS); + } + final Birthday modelBirthday = new Birthday(birthday); + + // Loan validity check + if (loan == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Loan.class.getSimpleName())); + } + if (!Loan.isValidLoan(loan)) { + throw new IllegalValueException(Loan.MESSAGE_CONSTRAINTS); + } + final Loan modelLoan = new Loan(loan); + + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelBirthday, + modelTags, modelLoan, modelHistory); } } diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..27326da1908 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -11,7 +11,9 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.note.Note; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * An Immutable AddressBook that is serializable to JSON format. @@ -19,16 +21,22 @@ @JsonRootName(value = "addressbook") class JsonSerializableAddressBook { + public static final String MESSAGE_DUPLICATE_NOTE = "Notes list contains duplicate note(s)."; public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; private final List<JsonAdaptedPerson> persons = new ArrayList<>(); + private final List<JsonAdaptedNote> notes = new ArrayList<>(); + private final List<JsonAdaptedTag> tags = new ArrayList<>(); + /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableAddressBook} with the given persons and tags. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List<JsonAdaptedPerson> persons) { + public JsonSerializableAddressBook(@JsonProperty("persons") List<JsonAdaptedPerson> persons, + @JsonProperty("tags") List<JsonAdaptedTag> tags) { this.persons.addAll(persons); + this.tags.addAll(tags); } /** @@ -38,6 +46,9 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List<JsonAdaptedPers */ public JsonSerializableAddressBook(ReadOnlyAddressBook source) { persons.addAll(source.getPersonList().stream().map(JsonAdaptedPerson::new).collect(Collectors.toList())); + notes.addAll(source.getNoteBook().stream().map(JsonAdaptedNote::new).collect(Collectors.toList())); + tags.addAll(source.getTagMapping().values().stream().map(JsonAdaptedTag::new).collect(Collectors.toList())); + } /** @@ -47,13 +58,42 @@ public JsonSerializableAddressBook(ReadOnlyAddressBook source) { */ public AddressBook toModelType() throws IllegalValueException { AddressBook addressBook = new AddressBook(); + List<Tag> addressBookTagList = new ArrayList<>(); + List<Person> addressBookPersonList = new ArrayList<>(); + + for (JsonAdaptedTag jsonAdaptedTag : tags) { + Tag tag = jsonAdaptedTag.toModelType(); + addressBook.addTag(tag); + addressBookTagList.add(tag); + } + for (JsonAdaptedPerson jsonAdaptedPerson : persons) { - Person person = jsonAdaptedPerson.toModelType(); + Person person = jsonAdaptedPerson.toModelType(addressBookTagList); if (addressBook.hasPerson(person)) { throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); } addressBook.addPerson(person); + addressBookPersonList.add(person); } + + for (JsonAdaptedNote jsonAdaptedNote : notes) { + Note note = jsonAdaptedNote.toModelType(addressBookTagList); + if (addressBook.hasNote(note)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_NOTE); + } + addressBook.addNote(note); + } + + // Add person references into each tag + for (Tag tag : addressBookTagList) { + for (Person person : addressBookPersonList) { + if (person.getTags().contains(tag)) { + tag.addPerson(person); + } + } + } + + return addressBook; } diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 9e75478664b..e7b1957d345 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -1,9 +1,14 @@ package seedu.address.ui; +import javafx.animation.FadeTransition; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.util.Duration; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; @@ -31,6 +36,26 @@ public CommandBox(CommandExecutor commandExecutor) { commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); } + /** + * Binds a result display below this command box + * @param resultDisplay the result display to bind + */ + public void bindResultsDisplay(StackPane resultDisplay) { + commandTextField.focusedProperty().addListener((obs, o, n) -> { + FadeTransition ft = new FadeTransition(Duration.millis(300), resultDisplay); + ft.setToValue(n ? 0.9 : 0); + ft.play(); + }); + + commandTextField.addEventHandler(KeyEvent.KEY_PRESSED, t -> { + if (t.getCode() == KeyCode.ESCAPE) { + resultDisplay.requestFocus(); + } + }); + + resultDisplay.requestFocus(); + } + /** * Handles the Enter button pressed event. */ @@ -69,6 +94,10 @@ private void setStyleToIndicateCommandFailure() { styleClass.add(ERROR_STYLE_CLASS); } + public TextField getCommandTextField() { + return commandTextField; + } + /** * Represents a function that can execute commands. */ diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..750e56f1900 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<Stage> { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2223s1-cs2103t-w12-2.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/InspectionPanel.java b/src/main/java/seedu/address/ui/InspectionPanel.java new file mode 100644 index 00000000000..428c669fb7a --- /dev/null +++ b/src/main/java/seedu/address/ui/InspectionPanel.java @@ -0,0 +1,160 @@ +package seedu.address.ui; + +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Logger; + +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.util.Pair; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.person.Loan; +import seedu.address.model.person.LoanHistory; +import seedu.address.model.person.Person; + +/** + * Panel containing the list of persons. + */ +public class InspectionPanel extends UiPart<Region> { + private static final String PERSON_IMAGE_PATH = "/images/person.png"; + private static final String PHONE_IMAGE_PATH = "/images/phone.png"; + private static final String EMAIL_IMAGE_PATH = "/images/mail.png"; + private static final String ADDRESS_IMAGE_PATH = "/images/home.png"; + private static final String BIRTHDAY_IMAGE_PATH = "/images/birthday.png"; + private static final String LOAN_IMAGE_PATH = "/images/loan.png"; + private static final String NO_RECORDS_IMAGE_PATH = "/images/no_records.png"; + + private static final String FXML = "InspectionPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(InspectionPanel.class); + + @FXML + private ListView<Pair<Loan, LoanHistory>> historyListView; + + @FXML + private Label name; + @FXML + private ImageView nameImage; + + @FXML + private Label phone; + @FXML + private ImageView phoneImage; + + @FXML + private Label address; + @FXML + private ImageView addressImage; + + @FXML + private Label email; + @FXML + private ImageView emailImage; + + @FXML + private Label birthday; + @FXML + private ImageView birthdayImage; + + @FXML + private VBox basicInformation; + + @FXML + private ImageView loanIndicator; + + @FXML + private ImageView noRecordsImage; + + @FXML + private Label summaryText; + + + /** + * Creates a {@code PersonListPanel} with the given {@code ObservableList}. + */ + public InspectionPanel(ListView<Person> personListView) { + super(FXML); + setImageViews(); + personListView.getSelectionModel().selectedItemProperty() + .addListener((obs, o, n) -> Optional.ofNullable(n).ifPresent(this::setInspectParameters)); + + basicInformation.maxWidthProperty().bind(getRoot().widthProperty().multiply(0.33)); + basicInformation.prefWidthProperty().bind(basicInformation.maxWidthProperty()); + + personListView.getSelectionModel().select(0); + } + + private void setInspectParameters(Person n) { + String fullName = n.getName().fullName; + name.setText(fullName); + email.setText(n.getEmail().value); + phone.setText(n.getPhone().value); + address.setText(n.getAddress().value); + birthday.setText(n.getBirthday().value); + + double loanAmount = n.getLoan().getAmount(); + if (loanAmount >= 0) { + summaryText.setText(String.format("%s is due to pay $%.2f", fullName, loanAmount)); + } else { + summaryText.setText(String.format("%s is to be paid $%.2f", fullName, -loanAmount)); + } + + historyListView.setItems(FXCollections.observableList(n.getHistoryWithTotal())); + historyListView.setCellFactory(listView -> new HistoryListViewCell()); + + if (historyListView.getItems().size() == 0) { + noRecordsImage.setOpacity(1); + loanIndicator.setOpacity(0); + summaryText.setOpacity(0); + } else { + noRecordsImage.setOpacity(0); + loanIndicator.setOpacity(1); + summaryText.setOpacity(1); + } + } + + private void setImageViews() { + nameImage.setImage( + new Image(Objects.requireNonNull(getClass().getResourceAsStream(PERSON_IMAGE_PATH)))); + phoneImage.setImage( + new Image(Objects.requireNonNull(getClass().getResourceAsStream(PHONE_IMAGE_PATH)))); + emailImage.setImage( + new Image(Objects.requireNonNull(getClass().getResourceAsStream(EMAIL_IMAGE_PATH)))); + addressImage.setImage( + new Image(Objects.requireNonNull(getClass().getResourceAsStream(ADDRESS_IMAGE_PATH)))); + birthdayImage.setImage( + new Image(Objects.requireNonNull(getClass().getResourceAsStream(BIRTHDAY_IMAGE_PATH)))); + loanIndicator.setImage( + new Image(Objects.requireNonNull(getClass().getResourceAsStream(LOAN_IMAGE_PATH)))); + noRecordsImage.setImage( + new Image(Objects.requireNonNull(getClass().getResourceAsStream(NO_RECORDS_IMAGE_PATH)))); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code LoanHistory} using a {@code LoanHistory}. + */ + class HistoryListViewCell extends ListCell<Pair<Loan, LoanHistory>> { + @Override + protected void updateItem(Pair<Loan, LoanHistory> totalAndHistory, boolean empty) { + super.updateItem(totalAndHistory, empty); + + if (empty || totalAndHistory == null) { + setGraphic(null); + setText(null); + } else { + LoanHistoryCard loanHistoryCard = new LoanHistoryCard(totalAndHistory.getValue(), + totalAndHistory.getKey().toString()); + + loanHistoryCard.bindWidth(historyListView, 15); + setGraphic(loanHistoryCard.getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/LoanHistoryCard.java b/src/main/java/seedu/address/ui/LoanHistoryCard.java new file mode 100644 index 00000000000..5479ccd0c41 --- /dev/null +++ b/src/main/java/seedu/address/ui/LoanHistoryCard.java @@ -0,0 +1,90 @@ +package seedu.address.ui; + +import java.util.Objects; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.person.LoanHistory; + + +/** + * An UI component that displays information of a {@code Person}. + */ +public class LoanHistoryCard extends UiPart<Region> { + + private static final String FXML = "LoanHistoryCard.fxml"; + + private static final String INCREASE_IMAGE_PATH = "/images/increase_arrow.png"; + private static final String DECREASE_IMAGE_PATH = "/images/decrease_arrow.png"; + + /** + * 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 <a href="https://github.com/se-edu/addressbook-level4/issues/336">The issue on AddressBook level 4</a> + */ + + public final LoanHistory history; + + @FXML + private HBox cardPane; + @FXML + private ImageView changeIdentifierImage; + @FXML + private Label currentLoanAmount; + @FXML + private Label changeInAmount; + @FXML + private Label reason; + + /** + * Creates a {@code PersonCode} with the given {@code Person} and index to display. + */ + public LoanHistoryCard(LoanHistory history, String updatedAmountString) { + super(FXML); + this.history = history; + currentLoanAmount.setText(updatedAmountString); + + changeIdentifierImage.setImage(new Image(Objects.requireNonNull(getClass() + .getResourceAsStream(history.getLoanChange().getAmount() > 0 + ? INCREASE_IMAGE_PATH + : DECREASE_IMAGE_PATH)) + )); + + changeInAmount.setText(this.history.getLoanChange().toString(true)); + + reason.setText(this.history.getReason().toString()); + } + + /** + * Binds the width of the contents of this card to the width of the list view + * @param listView the list view to bind to + * @param padding the padding applied after the width property + */ + public <T> void bindWidth(ListView<T> listView, double padding) { + getRoot().maxWidthProperty().bind(listView.widthProperty().subtract(padding)); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof LoanHistoryCard)) { + return false; + } + + // state check + LoanHistoryCard card = (LoanHistoryCard) other; + return history.equals(card.history); + } +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 9106c3aa6e5..c7f8b23cd52 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -1,28 +1,38 @@ package seedu.address.ui; +import java.util.Arrays; +import java.util.Timer; +import java.util.TimerTask; import java.util.logging.Logger; +import java.util.stream.Collectors; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.ListView; import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputControl; +import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.util.StringUtil; import seedu.address.logic.Logic; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Person; /** * The Main Window. Provides the basic application layout containing * a menu bar and space where other JavaFX elements can be placed. */ public class MainWindow extends UiPart<Stage> { - private static final String FXML = "MainWindow.fxml"; private final Logger logger = LogsCenter.getLogger(getClass()); @@ -31,7 +41,7 @@ public class MainWindow extends UiPart<Stage> { private Logic logic; // Independent Ui parts residing in this Ui container - private PersonListPanel personListPanel; + private WindowAnchorPane windowAnchorPane; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @@ -41,21 +51,25 @@ public class MainWindow extends UiPart<Stage> { @FXML private MenuItem helpMenuItem; - @FXML - private StackPane personListPanelPlaceholder; - @FXML private StackPane resultDisplayPlaceholder; @FXML private StackPane statusbarPlaceholder; + @FXML + private AnchorPane windowAnchorPaneHolder; + + private final Scene scene; + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ public MainWindow(Stage primaryStage, Logic logic) { super(FXML, primaryStage); + scene = primaryStage.getScene(); + // Set dependencies this.primaryStage = primaryStage; this.logic = logic; @@ -63,9 +77,12 @@ public MainWindow(Stage primaryStage, Logic logic) { // Configure the UI setWindowDefaultSize(logic.getGuiSettings()); - setAccelerators(); + // No more menu bar + //setAccelerators(); helpWindow = new HelpWindow(); + + scene.setOnMouseClicked(e -> resultDisplayPlaceholder.requestFocus()); } public Stage getPrimaryStage() { @@ -110,17 +127,48 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + windowAnchorPane = new WindowAnchorPane(logic); + windowAnchorPane.fillInnerParts(); + windowAnchorPaneHolder.getChildren().add(windowAnchorPane.getRoot()); + windowAnchorPane.resizeElements(primaryStage.getHeight(), + primaryStage.getWidth()); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); - statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); + // we removed the status bar + // StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); + // statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); CommandBox commandBox = new CommandBox(this::executeCommand); + commandBox.bindResultsDisplay(resultDisplayPlaceholder); + commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + resultDisplayPlaceholder.prefWidthProperty().bind(scene.widthProperty()); + + scene.addEventFilter(KeyEvent.KEY_PRESSED, t -> { + if (t.getCode() == KeyCode.SPACE && !commandBox.getCommandTextField().isFocused()) { + commandBox.getCommandTextField().setEditable(false); + commandBox.getCommandTextField().requestFocus(); + new Timer().schedule(new TimerTask() { + @Override + public void run() { + commandBox.getCommandTextField().setEditable(true); + commandBox.getCommandTextField().selectEnd(); + } + }, 10); + } + }); + + scene.heightProperty().addListener((ob, oldVal, newVal) -> + windowAnchorPane.resizeElements(scene.getHeight(), + scene.getWidth()) + ); + scene.widthProperty().addListener((ob, oldVal, newVal) -> + windowAnchorPane.resizeElements(scene.getHeight(), scene.getWidth()) + ); + + windowAnchorPane.resizeElements(scene.getHeight(), scene.getWidth()); } /** @@ -163,8 +211,66 @@ private void handleExit() { primaryStage.hide(); } + private void handleInspect(String[] inspectingName) { + if (inspectingName == null || inspectingName.length == 0) { + resultDisplay.setFeedbackToUser("There was nothing given to inspect"); + return; + } + + ListView<Person> personListView = getPersonListPanel().getListView(); + + try { + int index = Integer.parseInt(inspectingName[0]) - 1; + if (index < 0 || index >= personListView.getItems().size()) { + resultDisplay.setFeedbackToUser(Messages.OUT_OF_BOUNDS); + return; + } + + personListView.getSelectionModel().clearSelection(); + personListView.getSelectionModel().select(index); + return; + } catch (NumberFormatException e) { + logger.info(Messages.NOT_AN_INTEGER); + } + + Person[] personsArray = personListView.getItems() + .stream().filter(x -> matches(x.getName().fullName, inspectingName)) + .toArray(Person[]::new); + + if (personsArray.length == 0) { + resultDisplay.setFeedbackToUser(String.format(Messages.MESSAGE_INVALID_NAME_INSPECT, + Arrays.stream(inspectingName).map(n -> "\"" + n + "\"") + .collect(Collectors.joining(" or ")))); + return; + } + + if (personsArray.length > 1) { + resultDisplay.setFeedbackToUser(Messages.AMBIGUOUS_NAME_INSPECT_FIRST); + } + + int index = personListView.getItems().indexOf(personsArray[0]); + personListView.getSelectionModel().clearAndSelect(index); + } + + private void handleShowNotePanel(boolean isVisible) { + windowAnchorPane.setNotesPaneVisibility(isVisible, scene.getHeight(), scene.getWidth()); + } + + private boolean matches(String currentName, String[] matching) { + for (String word : matching) { + if (StringUtil.containsWordIgnoreCase(currentName, word)) { + return true; + } + } + return false; + } + public PersonListPanel getPersonListPanel() { - return personListPanel; + return windowAnchorPane.getPersonListPanel(); + } + + public NoteListPanel getNoteListPanel() { + return windowAnchorPane.getNoteListPanel(); } /** @@ -178,14 +284,37 @@ private CommandResult executeCommand(String commandText) throws CommandException logger.info("Result: " + commandResult.getFeedbackToUser()); resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); - if (commandResult.isShowHelp()) { + switch (commandResult.getUiState()) { + case ShowHelp: handleHelp(); - } + break; - if (commandResult.isExit()) { + case Exit: handleExit(); + break; + + case Inspect: + handleInspect(commandResult.getArgs()); + break; + + case HideNotes: + handleShowNotePanel(false); + break; + + case ShowNotes: + handleShowNotePanel(true); + break; + + default: + break; } + getPersonListPanel().setFilteredBoxIcon( + logic.getAddressBook().getPersonList().size() != logic.getFilteredPersonList().size()); + + getNoteListPanel().setFilteredBoxIcon( + logic.getAddressBook().getNoteBook().size() != logic.getFilteredNoteList().size()); + return commandResult; } catch (CommandException | ParseException e) { logger.info("Invalid command: " + commandText); diff --git a/src/main/java/seedu/address/ui/NoteCard.java b/src/main/java/seedu/address/ui/NoteCard.java new file mode 100644 index 00000000000..6bf3408951c --- /dev/null +++ b/src/main/java/seedu/address/ui/NoteCard.java @@ -0,0 +1,81 @@ +package seedu.address.ui; + +import java.util.Comparator; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.note.Note; + +/** + * An UI component that displays information of a {@code Person}. + */ +public class NoteCard extends UiPart<Region> { + + private static final String FXML = "NoteListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see <a href="https://github.com/se-edu/addressbook-level4/issues/336">The issue on AddressBook level 4</a> + */ + + public final Note note; + + @FXML + private HBox cardPane; + @FXML + private Label title; + @FXML + private Label id; + @FXML + private Label content; + @FXML + private FlowPane tags; + + /** + * Creates a {@code PersonCode} with the given {@code Person} and index to display. + */ + public NoteCard(Note note, int displayedIndex) { + super(FXML); + this.note = note; + id.setText(displayedIndex + ". "); + title.setText(note.getTitle().fullTitle); + content.setText(note.getContent().fullContent); + note.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + } + + /** + * Binds the width of the contents of this card to the width of the list view + * @param listView the list view to bind to + * @param padding the padding applied after the width property + */ + public void bindWidth(ListView<Note> listView, double padding) { + getRoot().maxWidthProperty().bind(listView.widthProperty().subtract(padding)); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof NoteCard)) { + return false; + } + + // state check + NoteCard card = (NoteCard) other; + return id.getText().equals(card.id.getText()) + && note.equals(card.note); + } +} diff --git a/src/main/java/seedu/address/ui/NoteListPanel.java b/src/main/java/seedu/address/ui/NoteListPanel.java new file mode 100644 index 00000000000..8398a41b471 --- /dev/null +++ b/src/main/java/seedu/address/ui/NoteListPanel.java @@ -0,0 +1,81 @@ +package seedu.address.ui; + +import java.util.Objects; +import java.util.logging.Logger; + +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.util.Duration; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.note.Note; + +/** + * Panel containing the list of persons. + */ +public class NoteListPanel extends UiPart<Region> { + private static final String FXML = "NoteListPanel.fxml"; + private static final String FILTER_IMAGE_PATH = "/images/filter.png"; + private final Logger logger = LogsCenter.getLogger(NoteListPanel.class); + + @FXML + private ListView<Note> noteListView; + @FXML + private ImageView notebookLogo; + + @FXML + private HBox filteredBox; + + @FXML + private ImageView filteredImage; + + /** + * Creates a {@code NoteListPanel} with the given {@code ObservableList}. + */ + public NoteListPanel(ObservableList<Note> noteList) { + super(FXML); + noteListView.setItems(noteList); + noteListView.setCellFactory(listView -> new NoteListViewCell()); + notebookLogo.setImage( + new Image(Objects.requireNonNull(getClass().getResource("/images/notebook.png")) + .toString())); + filteredImage.setImage( + new Image(Objects.requireNonNull(getClass().getResourceAsStream(FILTER_IMAGE_PATH)))); + filteredBox.setOpacity(0); + } + + public void setFilteredBoxIcon(boolean isVisible) { + Timeline timeline = new Timeline(); + timeline.getKeyFrames().add(new KeyFrame(Duration.millis(500), + new KeyValue(filteredBox.opacityProperty(), isVisible ? 1 : 0))); + timeline.play(); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Note} using a {@code NoteCard}. + */ + class NoteListViewCell extends ListCell<Note> { + @Override + protected void updateItem(Note note, boolean empty) { + super.updateItem(note, empty); + + if (empty || note == null) { + setGraphic(null); + setText(null); + } else { + NoteCard noteCard = new NoteCard(note, getIndex() + 1); + noteCard.bindWidth(noteListView, 20); + setGraphic(noteCard.getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 7fc927bc5d9..0961b54c1b6 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -1,9 +1,12 @@ package seedu.address.ui; import java.util.Comparator; +import java.util.Objects; import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; @@ -13,6 +16,8 @@ * An UI component that displays information of a {@code Person}. */ public class PersonCard extends UiPart<Region> { + private static final String PHONE_IMAGE_PATH = "/images/phone.png"; + private static final String LOAN_IMAGE_PATH = "/images/loan.png"; private static final String FXML = "PersonListCard.fxml"; @@ -35,9 +40,13 @@ public class PersonCard extends UiPart<Region> { @FXML private Label phone; @FXML - private Label address; + private ImageView phoneImage; @FXML - private Label email; + private Label loanAmount; + @FXML + private ImageView loanImage; + @FXML + private Label birthday; @FXML private FlowPane tags; @@ -47,11 +56,14 @@ public class PersonCard extends UiPart<Region> { public PersonCard(Person person, int displayedIndex) { super(FXML); this.person = person; - id.setText(displayedIndex + ". "); + id.setText("#" + displayedIndex); name.setText(person.getName().fullName); + phoneImage.setImage( + new Image(Objects.requireNonNull(getClass().getResourceAsStream(PHONE_IMAGE_PATH)))); phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); + loanImage.setImage( + new Image(Objects.requireNonNull(getClass().getResourceAsStream(LOAN_IMAGE_PATH)))); + loanAmount.setText(person.getLoan().toString()); person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index f4c501a897b..27ad03c9694 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -1,12 +1,20 @@ package seedu.address.ui; +import java.util.Objects; import java.util.logging.Logger; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.util.Duration; import seedu.address.commons.core.LogsCenter; import seedu.address.model.person.Person; @@ -15,11 +23,17 @@ */ public class PersonListPanel extends UiPart<Region> { private static final String FXML = "PersonListPanel.fxml"; + private static final String FILTER_IMAGE_PATH = "/images/filter.png"; private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); @FXML private ListView<Person> personListView; + @FXML + private ImageView filteredImage; + @FXML + private HBox filteredBox; + /** * Creates a {@code PersonListPanel} with the given {@code ObservableList}. */ @@ -27,12 +41,27 @@ public PersonListPanel(ObservableList<Person> personList) { super(FXML); personListView.setItems(personList); personListView.setCellFactory(listView -> new PersonListViewCell()); + filteredImage.setImage( + new Image(Objects.requireNonNull(getClass().getResourceAsStream(FILTER_IMAGE_PATH)))); + filteredBox.setOpacity(0); + } + + public void setFilteredBoxIcon(boolean isVisible) { + Timeline timeline = new Timeline(); + timeline.getKeyFrames().add(new KeyFrame(Duration.millis(500), + new KeyValue(filteredBox.opacityProperty(), isVisible ? 1 : 0))); + timeline.play(); + } + + public ListView<Person> getListView() { + return personListView; } /** * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. */ class PersonListViewCell extends ListCell<Person> { + @Override protected void updateItem(Person person, boolean empty) { super.updateItem(person, empty); @@ -41,9 +70,20 @@ protected void updateItem(Person person, boolean empty) { setGraphic(null); setText(null); } else { + if (personListView.getSelectionModel().getSelectedIndex() != getIndex()) { + setOpacity(0.7); + } setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); } + + focusedProperty().addListener((ob, o, n) -> setOpacity(1)); + + setOnMouseEntered(e -> setOpacity(1)); + setOnMouseExited(e -> { + if (personListView.getSelectionModel().getSelectedIndex() != getIndex()) { + setOpacity(0.7); + } + }); } } - } diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/address/ui/ResultDisplay.java index 7d98e84eedf..158cd9b78b5 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/address/ui/ResultDisplay.java @@ -16,6 +16,9 @@ public class ResultDisplay extends UiPart<Region> { @FXML private TextArea resultDisplay; + /** + * Displays the results of the command to the user + */ public ResultDisplay() { super(FXML); } diff --git a/src/main/java/seedu/address/ui/WindowAnchorPane.java b/src/main/java/seedu/address/ui/WindowAnchorPane.java new file mode 100644 index 00000000000..d3043a8e2a8 --- /dev/null +++ b/src/main/java/seedu/address/ui/WindowAnchorPane.java @@ -0,0 +1,163 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.value.WritableDoubleValue; +import javafx.fxml.FXML; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.util.Duration; +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.Logic; + +/** + * Panel containing the list of persons. + */ +public class WindowAnchorPane extends UiPart<Region> { + private static final double HORIZONTAL_DIVIDER = 0.45; + private static final double DISTANCE_BOTTOM = 0; + private static final double VERTICAL_DIVIDER_DEFAULT = 0.65; + + private static final String FXML = "WindowAnchorPane.fxml"; + private static final double OFFSET_HEIGHT = 60; + private static final double RIGHT_PADDING = 10; + private static final double OUT_OF_BOUNDS_RIGHT = -400; + private final Logger logger = LogsCenter.getLogger(WindowAnchorPane.class); + + private double verticalDivider = VERTICAL_DIVIDER_DEFAULT; + + private Logic logic; + private PersonListPanel personListPanel; + private NoteListPanel noteListPanel; + private InspectionPanel inspectionPanel; + + @FXML + private AnchorPane container; + + @FXML + private StackPane personListPanelPlaceholder; + + @FXML + private StackPane noteListPanelPlaceholder; + + @FXML + private StackPane inspectionPanelPlaceholder; + + @FXML + private VBox personList; + + @FXML + private VBox noteList; + + @FXML + private VBox inspectionSection; + + /** + * Creates a {@code PersonListPanel} with the given {@code ObservableList}. + */ + public WindowAnchorPane(Logic logic) { + super(FXML); + this.logic = logic; + } + + /** + * Fills up all the placeholders of this window. + */ + void fillInnerParts() { + personListPanel = new PersonListPanel(logic.getFilteredPersonList()); + personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + + inspectionPanel = new InspectionPanel(personListPanel.getListView()); + inspectionPanelPlaceholder.getChildren().add(inspectionPanel.getRoot()); + + noteListPanel = new NoteListPanel(logic.getFilteredNoteList()); + noteListPanelPlaceholder.getChildren().add(noteListPanel.getRoot()); + } + + /** + * Resizes the children elements inside the AnchorPane + * @param stageHeight the height of the stage this anchor pane is one + * @param stageWidth the width of the stage this anchor pane is one + */ + public void resizeElements(double stageHeight, double stageWidth) { + //Confine anchor pane container to width of stage + stageHeight -= OFFSET_HEIGHT; + + container.setMaxWidth(stageWidth); + container.setMaxHeight(stageHeight); + container.setPrefWidth(stageWidth); + container.setPrefHeight(stageHeight); + + //set anchors of personListPanel + AnchorPane.setLeftAnchor(personList, 0.0); + AnchorPane.setRightAnchor(personList, (1.0 - verticalDivider) * stageWidth); + AnchorPane.setTopAnchor(personList, 0.0); + AnchorPane.setBottomAnchor(personList, (1 - HORIZONTAL_DIVIDER) * stageHeight); + + //set anchors of inspectPanel + AnchorPane.setLeftAnchor(inspectionSection, 0.0); + AnchorPane.setRightAnchor(inspectionSection, (1.0 - verticalDivider) * stageWidth); + AnchorPane.setTopAnchor(inspectionSection, HORIZONTAL_DIVIDER * stageHeight); + AnchorPane.setBottomAnchor(inspectionSection, DISTANCE_BOTTOM); + + + //set anchors of noteListPanel + AnchorPane.setLeftAnchor(noteList, (verticalDivider) * stageWidth); + AnchorPane.setRightAnchor(noteList, stageWidth * (VERTICAL_DIVIDER_DEFAULT - verticalDivider)); + AnchorPane.setTopAnchor(noteList, 10.0); + AnchorPane.setBottomAnchor(noteList, DISTANCE_BOTTOM); + + container.autosize(); + } + + public PersonListPanel getPersonListPanel() { + return personListPanel; + } + + public NoteListPanel getNoteListPanel() { + return noteListPanel; + } + + public void setNotesPaneVisibility(boolean isVisible, double stageHeight, double stageWidth) { + Timeline anim = new Timeline(); + anim.getKeyFrames().add(new KeyFrame(Duration.millis(300), new KeyValue( + new WritableDoubleValue() { + @Override + public double get() { + return WindowAnchorPane.this.verticalDivider; + } + + @Override + public void set(double value) { + verticalDivider = value; + resizeElements(stageHeight, stageWidth); + } + + @Override + public void setValue(Number value) { + verticalDivider = (double) value; + resizeElements(stageHeight, stageWidth); + } + + @Override + public Number getValue() { + return WindowAnchorPane.this.verticalDivider; + } + }, + isVisible ? VERTICAL_DIVIDER_DEFAULT : 1, + Interpolator.LINEAR))); + + anim.getKeyFrames().add(new KeyFrame(Duration.millis(300), new KeyValue( + noteList.opacityProperty(), + isVisible ? 1 : 0, + Interpolator.LINEAR))); + + anim.play(); + } +} diff --git a/src/main/resources/fonts/Bender-Black.otf b/src/main/resources/fonts/Bender-Black.otf new file mode 100644 index 00000000000..7e4b1d592fa Binary files /dev/null and b/src/main/resources/fonts/Bender-Black.otf differ diff --git a/src/main/resources/fonts/Bender-Bold.otf b/src/main/resources/fonts/Bender-Bold.otf new file mode 100644 index 00000000000..204e9e619cc Binary files /dev/null and b/src/main/resources/fonts/Bender-Bold.otf differ diff --git a/src/main/resources/fonts/Bender-Light.otf b/src/main/resources/fonts/Bender-Light.otf new file mode 100644 index 00000000000..bc0cadafcad Binary files /dev/null and b/src/main/resources/fonts/Bender-Light.otf differ diff --git a/src/main/resources/fonts/Bender.otf b/src/main/resources/fonts/Bender.otf new file mode 100644 index 00000000000..e2ae7ee4936 Binary files /dev/null and b/src/main/resources/fonts/Bender.otf differ diff --git a/src/main/resources/fonts/MinionPro-Bold.otf b/src/main/resources/fonts/MinionPro-Bold.otf new file mode 100644 index 00000000000..1dc230fa8b1 Binary files /dev/null and b/src/main/resources/fonts/MinionPro-Bold.otf differ diff --git a/src/main/resources/fonts/MinionPro-Medium.otf b/src/main/resources/fonts/MinionPro-Medium.otf new file mode 100644 index 00000000000..76a2c181412 Binary files /dev/null and b/src/main/resources/fonts/MinionPro-Medium.otf differ diff --git a/src/main/resources/fonts/MinionPro-Semibold.otf b/src/main/resources/fonts/MinionPro-Semibold.otf new file mode 100644 index 00000000000..22525aef3eb Binary files /dev/null and b/src/main/resources/fonts/MinionPro-Semibold.otf differ diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png index 29810cf1fd9..3bea0752ed5 100644 Binary files a/src/main/resources/images/address_book_32.png and b/src/main/resources/images/address_book_32.png differ diff --git a/src/main/resources/images/bg.png b/src/main/resources/images/bg.png new file mode 100644 index 00000000000..4e5cec32c27 Binary files /dev/null and b/src/main/resources/images/bg.png differ diff --git a/src/main/resources/images/birthday.png b/src/main/resources/images/birthday.png new file mode 100644 index 00000000000..f786cd0a1ea Binary files /dev/null and b/src/main/resources/images/birthday.png differ diff --git a/src/main/resources/images/decrease_arrow.png b/src/main/resources/images/decrease_arrow.png new file mode 100644 index 00000000000..ef9f53d5278 Binary files /dev/null and b/src/main/resources/images/decrease_arrow.png differ diff --git a/src/main/resources/images/filter.png b/src/main/resources/images/filter.png new file mode 100644 index 00000000000..19c7571e6d8 Binary files /dev/null and b/src/main/resources/images/filter.png differ diff --git a/src/main/resources/images/home.png b/src/main/resources/images/home.png new file mode 100644 index 00000000000..d10e62fd062 Binary files /dev/null and b/src/main/resources/images/home.png differ diff --git a/src/main/resources/images/increase_arrow.png b/src/main/resources/images/increase_arrow.png new file mode 100644 index 00000000000..fb7739be0c1 Binary files /dev/null and b/src/main/resources/images/increase_arrow.png differ diff --git a/src/main/resources/images/indicator_arrow.png b/src/main/resources/images/indicator_arrow.png new file mode 100644 index 00000000000..8d81b2d072b Binary files /dev/null and b/src/main/resources/images/indicator_arrow.png differ diff --git a/src/main/resources/images/loan.png b/src/main/resources/images/loan.png new file mode 100644 index 00000000000..ee5f0b64545 Binary files /dev/null and b/src/main/resources/images/loan.png differ diff --git a/src/main/resources/images/mail.png b/src/main/resources/images/mail.png new file mode 100644 index 00000000000..2bf0cda7f91 Binary files /dev/null and b/src/main/resources/images/mail.png differ diff --git a/src/main/resources/images/no_records.png b/src/main/resources/images/no_records.png new file mode 100644 index 00000000000..9baa2f74743 Binary files /dev/null and b/src/main/resources/images/no_records.png differ diff --git a/src/main/resources/images/notebook.png b/src/main/resources/images/notebook.png new file mode 100644 index 00000000000..fe5a42f0c17 Binary files /dev/null and b/src/main/resources/images/notebook.png differ diff --git a/src/main/resources/images/person.png b/src/main/resources/images/person.png new file mode 100644 index 00000000000..9a2d239b4d6 Binary files /dev/null and b/src/main/resources/images/person.png differ diff --git a/src/main/resources/images/phone.png b/src/main/resources/images/phone.png new file mode 100644 index 00000000000..b61c9b3014f Binary files /dev/null and b/src/main/resources/images/phone.png differ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 09f6d6fe9e4..ef2166d8fa0 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -3,7 +3,7 @@ <?import javafx.scene.control.TextField?> <?import javafx.scene.layout.StackPane?> -<StackPane styleClass="stack-pane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> +<StackPane styleClass="stack-pane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" opacity="0.5"> <TextField fx:id="commandTextField" onAction="#handleCommandEntered" promptText="Enter command here..."/> </StackPane> diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..b5478545518 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,32 +1,44 @@ +* { + -fx-bg-color: Gainsboro; + -fx-contrast-color: #F2D492; + -fx-highlight-color: #B8B08D; + -fx-shadow-color: #2C3333; + -fx-midtone-color: #544464; + -fx-darktone-color: #29153D; +} + .background { - -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ + -fx-background-color: radial-gradient(focus-distance 0% , center 50% 50% , radius 55% , AliceBlue, -fx-bg-color); + background-color: -fx-bg-color; /* Used in the default.html file */ + -fx-background-image: url("/images/bg.png"); + -fx-background-size: cover; } .label { -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; + -fx-font-family: 'MinionPro-Semibold'; -fx-text-fill: #555555; -fx-opacity: 0.9; } .label-bright { -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; + -fx-font-family: 'MinionPro-Semibold'; -fx-text-fill: white; -fx-opacity: 1; } .label-header { - -fx-font-size: 32pt; - -fx-font-family: "Segoe UI Light"; + -fx-font-size: 21pt; + -fx-font-family: 'Bender-Bold'; -fx-text-fill: white; -fx-opacity: 1; } .text-field { -fx-font-size: 12pt; - -fx-font-family: "Segoe UI Semibold"; + -fx-background-color: -fx-bg-color; + -fx-font-family: 'MinionPro-Semibold' } .tab-pane { @@ -40,9 +52,9 @@ } .table-view { - -fx-base: #1d1d1d; - -fx-control-inner-background: #1d1d1d; - -fx-background-color: #1d1d1d; + -fx-base: -fx-bg-color; + -fx-control-inner-background: -fx-bg-color; + -fx-background-color: -fx-bg-color; -fx-table-cell-border-color: transparent; -fx-table-header-border-color: transparent; -fx-padding: 5; @@ -66,7 +78,7 @@ .table-view .column-header .label { -fx-font-size: 20pt; - -fx-font-family: "Segoe UI Light"; + -fx-font-family: 'Bender-Bold Regular'; -fx-text-fill: white; -fx-alignment: center-left; -fx-opacity: 1; @@ -77,58 +89,144 @@ } .split-pane:horizontal .split-pane-divider { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(-fx-bg-color, 20%); -fx-border-color: transparent transparent transparent #4d4d4d; } .split-pane { -fx-border-radius: 1; -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(-fx-bg-color, 20%); +} + +.notes-VBox { + -fx-background-color: rgba(0, 0, 0, 0.4); + -fx-background-radius: 0; + -fx-padding: 5, 5, 5, 5; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 10, 0.5, -5.0, 0.0); +} + +.person-VBox { + -fx-background-color: transparent; + -fx-background-radius: 0; +} + +.filteredBox { + -fx-background-color: white; +} + +.filteredBox .label { + -fx-font-family: 'Bender-Bold'; +} + +.label#invertedScheme { + -fx-background-color: black; + -fx-text-fill: white; +} + +.label#normalScheme { + -fx-background-color: white; + -fx-text-fill: black; +} + +.separator-text *.line { + -fx-border-color: white; +} + +.separator-inspect *.line { + -fx-border-color: linear-gradient(transparent 0%, orange 50%, transparent 100%); + -fx-border-width: 1; +} + +.separator-card *.line { + -fx-border-color: linear-gradient(to right, transparent 0%, orangered 50%, transparent 100%); + -fx-border-width: 1; +} + +.separator-history *.line { + -fx-border-color: grey; + -fx-border-width: 1; +} + +.inspectionPanel-VBox { + -fx-background-color: rgba(0, 0, 0, 0.4); + -fx-padding: 5, 5, 5, 5; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 10, 0.5, -5.0, 0.0); +} + +.inspection-card .label { + -fx-text-fill: white; +} + +.history-panel .label { + -fx-text-fill: white; } .list-view { -fx-background-insets: 0; -fx-padding: 0; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: transparent; } .list-cell { -fx-label-padding: 0 0 0 0; -fx-graphic-text-gap : 0; - -fx-padding: 0 0 0 0; + -fx-padding: 4; + -fx-background-color: transparent; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 10, 0.5, -1.0, 1.0); +} + +.list-cell:filled { + -fx-background-color: rgba(0, 0, 0, 0.5); + -fx-background-insets: 4px ; } -.list-cell:filled:even { - -fx-background-color: #3c3e3f; +.person-list .list-cell:filled { + -fx-background-color: #303030; + -fx-background-radius: 5; + -fx-border-width: 3; + -fx-border-color: #f7bf97; + -fx-border-insets: 4px; } -.list-cell:filled:odd { - -fx-background-color: #515658; +.person-list .list-cell:filled:selected #cardPane { + -fx-border-color: #3e7b91; + -fx-border-width: 0; + -fx-border-radius: 0px; } + .list-cell:filled:selected { - -fx-background-color: #424d5f; + -fx-background-color: rgba(0, 0, 0, 0.8); + -fx-border-radius: 0px; } .list-cell:filled:selected #cardPane { -fx-border-color: #3e7b91; - -fx-border-width: 1; + -fx-border-width: 0; + -fx-border-radius: 0px; } .list-cell .label { -fx-text-fill: white; } -.cell_big_label { - -fx-font-family: "Segoe UI Semibold"; - -fx-font-size: 16px; +.cell_bigger_label { + -fx-font-family: 'MinionPro-Bold'; + -fx-font-size: 21px; -fx-text-fill: #010504; } +.person-VBox .cell_big_label { + -fx-padding: 5 5 5 5; + -fx-font-family: 'MinionPro-Bold'; + -fx-font-size: 16px; + -fx-text-fill: white; +} + .cell_small_label { - -fx-font-family: "Segoe UI"; - -fx-font-size: 13px; + -fx-font-family: "MinionPro-Semibold"; + -fx-font-size: 12px; -fx-text-fill: #010504; } @@ -137,18 +235,18 @@ } .pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); + -fx-background-color: transparent; + /*-fx-border-color: derive(-fx-bg-color, 10%);*/ -fx-border-top-width: 1px; } .status-bar { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(-fx-bg-color, 10%); } .result-display { -fx-background-color: transparent; - -fx-font-family: "Segoe UI Light"; + -fx-font-family: 'MinionPro-Medium'; -fx-font-size: 13pt; -fx-text-fill: white; } @@ -158,8 +256,8 @@ } .status-bar .label { - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-font-family: 'Bender'; + -fx-text-fill: black; -fx-padding: 4px; -fx-pref-height: 30px; } @@ -198,7 +296,7 @@ .menu-bar .label { -fx-font-size: 14pt; - -fx-font-family: "Segoe UI Light"; + -fx-font-family: 'Minion Pro'; -fx-text-fill: white; -fx-opacity: 0.9; } @@ -218,7 +316,7 @@ -fx-border-width: 2; -fx-background-radius: 0; -fx-background-color: #1d1d1d; - -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; + -fx-font-family: 'Bender-Bold', Helvetica, Arial, sans-serif; -fx-font-size: 11pt; -fx-text-fill: #d8d8d8; -fx-background-insets: 0 0 0 0, 0, 1, 2; @@ -282,12 +380,28 @@ } .scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: transparent; + -fx-background-radius: 20; + -fx-padding: 1; +} + + +.list-view .corner{ + -fx-background-color:transparent; +} + +.scroll-bar:horizontal { + -fx-pref-height: 13.5; +} + +.scroll-bar:vertical { + -fx-pref-width: 13.5; } .scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: derive(-fx-shadow-color, 10%); -fx-background-insets: 3; + -fx-background-radius: 20; } .scroll-bar .increment-button, .scroll-bar .decrement-button { @@ -323,7 +437,7 @@ -fx-border-color: #383838 #383838 #ffffff #383838; -fx-border-insets: 0; -fx-border-width: 1; - -fx-font-family: "Segoe UI Light"; + -fx-font-family: Helvetica; -fx-font-size: 13pt; -fx-text-fill: white; } @@ -343,10 +457,10 @@ } #tags .label { - -fx-text-fill: white; - -fx-background-color: #3e7b91; + -fx-text-fill: black; + -fx-background-color: lightcyan; -fx-padding: 1 3 1 3; -fx-border-radius: 2; -fx-background-radius: 2; - -fx-font-size: 11; + -fx-font-size: 8; } diff --git a/src/main/resources/view/InspectionPanel.fxml b/src/main/resources/view/InspectionPanel.fxml new file mode 100644 index 00000000000..db4c1804a3d --- /dev/null +++ b/src/main/resources/view/InspectionPanel.fxml @@ -0,0 +1,118 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.Insets?> +<?import javafx.scene.layout.HBox?> + +<?import javafx.scene.layout.VBox?> +<?import javafx.scene.control.Label?> +<?import javafx.scene.layout.FlowPane?> +<?import javafx.scene.control.ListView?> + +<?import javafx.scene.layout.GridPane?> +<?import javafx.scene.layout.ColumnConstraints?> +<?import javafx.scene.image.ImageView?> + +<?import javafx.scene.control.Separator?> +<?import javafx.scene.layout.StackPane?> +<VBox xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <Separator maxWidth="100" styleClass="separator-text"> + <padding> + <Insets top="10" bottom="5"/> + </padding> + </Separator> + <Label id="normalScheme" text="Inspect" styleClass="label-header"> + <padding> + <Insets left="20" top="5" bottom="5" right="10"/> + </padding> + </Label> + <Separator maxWidth="150" styleClass="separator-text"> + <padding> + <Insets top="5" bottom="10"/> + </padding> + </Separator> + <HBox styleClass="inspectionPanel-VBox"> + <padding> + <Insets top="10" right="10" bottom="10" left="10" /> + </padding> + <VBox fx:id="basicInformation" minWidth="250" prefWidth="250" maxWidth="250"> + <GridPane HBox.hgrow="NEVER"> + <columnConstraints> + <ColumnConstraints hgrow="SOMETIMES" minWidth="10" prefWidth="250" /> + </columnConstraints> + <VBox alignment="CENTER" minHeight="30" GridPane.columnIndex="0" spacing="5" styleClass="inspection-card"> + <padding> + <Insets top="10" right="15" bottom="5" left="15" /> + </padding> + <Label text="Basic Information" styleClass="cell_bigger_label"/> + <Separator/> + <VBox spacing="5" alignment="CENTER_LEFT" styleClass="inspection-card"> + <padding> + <Insets top="5" /> + </padding> + <HBox alignment="CENTER_LEFT" spacing="10"> + <ImageView fx:id="nameImage" fitHeight="20" fitWidth="20"/> + <Label fx:id="name" text="\$first" styleClass="cell_big_label" wrapText="true"/> + </HBox> + <HBox alignment="CENTER_LEFT" spacing="10"> + <ImageView fx:id="birthdayImage" fitHeight="20" fitWidth="20"/> + <Label fx:id="birthday" text="\$bday" styleClass="cell_big_label" wrapText="true"/> + </HBox> + <HBox alignment="CENTER_LEFT" spacing="10"> + <ImageView fx:id="phoneImage" fitHeight="20" fitWidth="20"/> + <Label fx:id="phone" styleClass="cell_big_label" text="\$phone" wrapText="true"/> + </HBox> + <HBox alignment="CENTER_LEFT" spacing="10"> + <ImageView fx:id="addressImage" fitHeight="20" fitWidth="20"/> + <Label fx:id="address" styleClass="cell_big_label" text="\$address" wrapText="true"/> + </HBox> + <HBox alignment="CENTER_LEFT" spacing="10"> + <ImageView fx:id="emailImage" fitHeight="20" fitWidth="20"/> + <Label fx:id="email" styleClass="cell_big_label" text="\$email" wrapText="true" /> + </HBox> + <FlowPane fx:id="tags"/> + </VBox> + </VBox> + </GridPane> + </VBox> + <HBox alignment="CENTER"> + <padding> + <Insets left="5" right="5"/> + </padding> + <Separator orientation="VERTICAL" styleClass="separator-inspect"/> + </HBox> + <VBox HBox.hgrow="ALWAYS"> + <padding> + <Insets left="15" right="15"/> + </padding> + <HBox styleClass="history-panel"> + <Label alignment="CENTER" minWidth="71" maxWidth="71" text="Total Loan" styleClass="cell_big_label"/> + <Label alignment="CENTER" minWidth="121" maxWidth="121" text="Change" styleClass="cell_big_label"/> + <Label alignment="CENTER" minWidth="70" maxWidth="70" text="Reason" styleClass="cell_big_label"/> + </HBox> + <HBox> + <HBox> + <padding> + <Insets top="5"/> + </padding> + <ImageView fx:id="loanIndicator" fitWidth="20" fitHeight="20"/> + </HBox> + <StackPane HBox.hgrow="ALWAYS"> + <VBox styleClass="history-panel"> + <padding> + <Insets top="5" bottom="5"/> + </padding> + <ListView fx:id="historyListView" HBox.hgrow="ALWAYS"/> + <HBox alignment="CENTER_RIGHT"> + <Label id="normalScheme" fx:id="summaryText" text="summary" styleClass="cell_small_label"> + <padding> + <Insets topRightBottomLeft="5"/> + </padding> + </Label> + </HBox> + </VBox> + <ImageView fx:id="noRecordsImage" fitHeight="120" preserveRatio="true"/> + </StackPane> + </HBox> + </VBox> + </HBox> +</VBox> diff --git a/src/main/resources/view/LoanHistoryCard.fxml b/src/main/resources/view/LoanHistoryCard.fxml new file mode 100644 index 00000000000..f158b5306d5 --- /dev/null +++ b/src/main/resources/view/LoanHistoryCard.fxml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.Insets?> +<?import javafx.scene.control.Label?> +<?import javafx.scene.layout.HBox?> + +<?import javafx.scene.control.Separator?> +<?import javafx.scene.image.ImageView?> +<HBox id="cardPane" fx:id="cardPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <HBox alignment="CENTER" minWidth="70" prefWidth="70" maxWidth="70"> + <padding> + <Insets left="10" right="10"/> + </padding> + <Label fx:id="currentLoanAmount" text="\$999999" styleClass="cell_small_label" wrapText="true"/> + </HBox> + <Separator orientation="VERTICAL" styleClass="separator-history"/> + <HBox alignment="CENTER_LEFT" minWidth="100" prefWidth="100" maxWidth="100" spacing="5"> + <padding> + <Insets left="10" right="10"/> + </padding> + <ImageView fx:id="changeIdentifierImage" fitWidth="15" fitHeight="15"/> + <Label fx:id="changeInAmount" text="\$changeAmount" styleClass="cell_small_label" wrapText="true"/> + </HBox> + <Separator orientation="VERTICAL" styleClass="separator-history"/> + <HBox HBox.hgrow="ALWAYS"> + <padding> + <Insets left="20"/> + </padding> + <Label fx:id="reason" text="\$reason" maxHeight="Infinity" styleClass="cell_small_label" wrapText="true"/> + </HBox> +</HBox> diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index a431648f6c0..2b4e5fe752c 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -3,16 +3,13 @@ <?import java.net.URL?> <?import javafx.geometry.Insets?> <?import javafx.scene.Scene?> -<?import javafx.scene.control.Menu?> -<?import javafx.scene.control.MenuBar?> -<?import javafx.scene.control.MenuItem?> -<?import javafx.scene.control.SplitPane?> <?import javafx.scene.image.Image?> <?import javafx.scene.layout.StackPane?> <?import javafx.scene.layout.VBox?> +<?import javafx.scene.layout.AnchorPane?> <fx:root type="javafx.stage.Stage" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" - title="Address App" minWidth="450" minHeight="600" onCloseRequest="#handleExit"> + title="Address App" minWidth="1024" minHeight="640" onCloseRequest="#handleExit"> <icons> <Image url="@/images/address_book_32.png" /> </icons> @@ -23,37 +20,27 @@ <URL value="@Extensions.css" /> </stylesheets> - <VBox> - <MenuBar fx:id="menuBar" VBox.vgrow="NEVER"> - <Menu mnemonicParsing="false" text="File"> - <MenuItem mnemonicParsing="false" onAction="#handleExit" text="Exit" /> - </Menu> - <Menu mnemonicParsing="false" text="Help"> - <MenuItem fx:id="helpMenuItem" mnemonicParsing="false" onAction="#handleHelp" text="Help" /> - </Menu> - </MenuBar> - + <VBox styleClass="background"> <StackPane VBox.vgrow="NEVER" fx:id="commandBoxPlaceholder" styleClass="pane-with-border"> <padding> <Insets top="5" right="10" bottom="5" left="10" /> </padding> </StackPane> - <StackPane VBox.vgrow="NEVER" fx:id="resultDisplayPlaceholder" styleClass="pane-with-border" - minHeight="100" prefHeight="100" maxHeight="100"> - <padding> - <Insets top="5" right="10" bottom="5" left="10" /> - </padding> - </StackPane> - - <VBox fx:id="personList" styleClass="pane-with-border" minWidth="340" prefWidth="340" VBox.vgrow="ALWAYS"> - <padding> - <Insets top="10" right="10" bottom="10" left="10" /> - </padding> - <StackPane fx:id="personListPanelPlaceholder" VBox.vgrow="ALWAYS"/> - </VBox> + <AnchorPane VBox.vgrow="ALWAYS"> + <AnchorPane VBox.vgrow="ALWAYS" fx:id = "windowAnchorPaneHolder" styleClass="pane-with-border" + minHeight="200" prefHeight="200" maxHeight="Infinity" + AnchorPane.leftAnchor="0" AnchorPane.topAnchor="0"/> - <StackPane fx:id="statusbarPlaceholder" VBox.vgrow="NEVER" /> + <StackPane VBox.vgrow="NEVER" fx:id="resultDisplayPlaceholder" styleClass="pane-with-border" + minHeight="150" prefHeight="150" + AnchorPane.leftAnchor="0" AnchorPane.topAnchor="0" + opacity="0" mouseTransparent="true"> + <padding> + <Insets top="5" right="10" bottom="5" left="10" /> + </padding> + </StackPane> + </AnchorPane> </VBox> </Scene> </scene> diff --git a/src/main/resources/view/NoteListCard.fxml b/src/main/resources/view/NoteListCard.fxml new file mode 100644 index 00000000000..1c6b0b59bc8 --- /dev/null +++ b/src/main/resources/view/NoteListCard.fxml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.Insets?> +<?import javafx.scene.control.Label?> +<?import javafx.scene.layout.ColumnConstraints?> +<?import javafx.scene.layout.FlowPane?> +<?import javafx.scene.layout.GridPane?> +<?import javafx.scene.layout.HBox?> +<?import javafx.scene.layout.Region?> +<?import javafx.scene.layout.VBox?> + +<HBox id="cardPane" fx:id="cardPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <padding> + <Insets top="15" right="15" bottom="15" left="15" /> + </padding> + <VBox alignment="CENTER_LEFT" minHeight="30" spacing="5" HBox.hgrow="ALWAYS"> + <HBox spacing="5" alignment="TOP_LEFT"> + <Label fx:id="id" styleClass="cell_big_label"> + <minWidth> + <!-- Ensures that the label text is never truncated --> + <Region fx:constant="USE_PREF_SIZE" /> + </minWidth> + </Label> + <Label fx:id="title" text="\$first" styleClass="cell_big_label" wrapText="true"/> + </HBox> + <Label fx:id="content" styleClass="cell_small_label" text="\$address" wrapText="true"/> + <Region VBox.Vgrow="ALWAYS"/> + <FlowPane alignment="BOTTOM_RIGHT" fx:id="tags"/> + </VBox> +</HBox> diff --git a/src/main/resources/view/NoteListPanel.fxml b/src/main/resources/view/NoteListPanel.fxml new file mode 100644 index 00000000000..9808f3310ab --- /dev/null +++ b/src/main/resources/view/NoteListPanel.fxml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.control.ListView?> +<?import javafx.scene.layout.VBox?> +<?import javafx.geometry.Insets?> +<?import javafx.scene.control.Label?> + +<?import javafx.scene.control.Separator?> +<?import javafx.scene.layout.Region?> +<?import javafx.scene.layout.AnchorPane?> +<?import javafx.scene.image.ImageView?> +<?import javafx.scene.layout.HBox?> +<VBox xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" styleClass="notes-VBox"> + <Separator maxWidth="150"> + <padding> + <Insets top="10" bottom="5" left="10"/> + </padding> + </Separator> + <HBox> + <padding> + <Insets left="10"/> + </padding> + <Label id="normalScheme" text="Notes" styleClass="label-header"> + <padding> + <Insets left="20" top="2" bottom="2" right="10"/> + </padding> + </Label> + <HBox alignment="BOTTOM_LEFT"> + <padding> + <Insets left="10"/> + </padding> + <HBox fx:id="filteredBox" alignment="CENTER" styleClass="filteredBox" maxHeight="10"> + <ImageView fx:id="filteredImage" fitHeight="20" fitWidth="20"/> + <Label text="FILTERED" styleClass="cell_small_label"> + <padding> + <Insets topRightBottomLeft="5"/> + </padding> + </Label> + </HBox> + </HBox> + </HBox> + <Separator maxWidth="100"> + <padding> + <Insets top="5" bottom="5" left="10"/> + </padding> + </Separator> + <AnchorPane VBox.vgrow="ALWAYS"> + <ImageView fx:id="notebookLogo" fitHeight="100" fitWidth="100" opacity="0.6" + AnchorPane.bottomAnchor="0" + AnchorPane.rightAnchor="0"/> + <ListView fx:id="noteListView" styleClass="notes-list" + AnchorPane.bottomAnchor="0" AnchorPane.topAnchor="0" + AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0"/> + </AnchorPane> +</VBox> diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f08ea32ad55..a8bd30c832c 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -2,35 +2,37 @@ <?import javafx.geometry.Insets?> <?import javafx.scene.control.Label?> -<?import javafx.scene.layout.ColumnConstraints?> <?import javafx.scene.layout.FlowPane?> -<?import javafx.scene.layout.GridPane?> <?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.Region?> <?import javafx.scene.layout.VBox?> - -<HBox id="cardPane" fx:id="cardPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> - <GridPane HBox.hgrow="ALWAYS"> - <columnConstraints> - <ColumnConstraints hgrow="SOMETIMES" minWidth="10" prefWidth="150" /> - </columnConstraints> - <VBox alignment="CENTER_LEFT" minHeight="105" GridPane.columnIndex="0"> +<?import javafx.scene.control.Separator?> +<?import javafx.scene.image.ImageView?> +<HBox id="cardPane" fx:id="cardPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" + prefHeight="160" maxWidth="120" alignment="CENTER"> + <VBox alignment="CENTER" minHeight="30"> + <VBox alignment="CENTER_LEFT" VBox.vgrow="ALWAYS" prefHeight="80"> + <VBox VBox.vgrow="ALWAYS"> + <Label fx:id="id" styleClass="cell_big_label" text="\$id" wrapText="true"/> + <VBox VBox.vgrow="ALWAYS" alignment="CENTER"> + <Label fx:id="name" textAlignment="CENTER" text="\$first" styleClass="cell_big_label" wrapText="true"/> + </VBox> + </VBox> + </VBox> + <Separator styleClass="separator-card" maxWidth="80" mouseTransparent="true"> <padding> - <Insets top="5" right="5" bottom="5" left="15" /> + <Insets top="5" bottom="10"/> </padding> - <HBox spacing="5" alignment="CENTER_LEFT"> - <Label fx:id="id" styleClass="cell_big_label"> - <minWidth> - <!-- Ensures that the label text is never truncated --> - <Region fx:constant="USE_PREF_SIZE" /> - </minWidth> - </Label> - <Label fx:id="name" text="\$first" styleClass="cell_big_label" /> - </HBox> - <FlowPane fx:id="tags" /> - <Label fx:id="phone" styleClass="cell_small_label" text="\$phone" /> - <Label fx:id="address" styleClass="cell_small_label" text="\$address" /> - <Label fx:id="email" styleClass="cell_small_label" text="\$email" /> - </VBox> - </GridPane> + </Separator> + <HBox alignment="CENTER_LEFT" spacing="10"> + <ImageView fx:id="phoneImage" fitHeight="20" fitWidth="20"/> + <Label fx:id="phone" styleClass="cell_small_label" text="\$phone" wrapText="true"/> + </HBox> + <HBox alignment="CENTER_LEFT" spacing="10"> + <ImageView fx:id="loanImage" fitHeight="20" fitWidth="20"/> + <Label fx:id="loanAmount" styleClass="cell_small_label" text="\$loan" wrapText="true" /> + </HBox> + <Region VBox.vgrow="ALWAYS"/> + <FlowPane alignment="BOTTOM_RIGHT" fx:id="tags"/> + </VBox> </HBox> diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml index 8836d323cc5..02cfc3b7496 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/PersonListPanel.fxml @@ -3,6 +3,41 @@ <?import javafx.scene.control.ListView?> <?import javafx.scene.layout.VBox?> -<VBox xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> - <ListView fx:id="personListView" VBox.vgrow="ALWAYS" /> +<?import javafx.scene.layout.HBox?> +<?import javafx.geometry.Insets?> +<?import javafx.scene.control.Label?> +<?import javafx.scene.control.Separator?> +<?import javafx.scene.image.ImageView?> +<VBox xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" styleClass="person-VBox"> + <Separator maxWidth="100" styleClass="separator-text"> + <padding> + <Insets top="10" bottom="5"/> + </padding> + </Separator> + <HBox> + <Label id="normalScheme" text="People" styleClass="label-header"> + <padding> + <Insets left="20" top="5" bottom="5" right="10"/> + </padding> + </Label> + <HBox alignment="BOTTOM_LEFT"> + <padding> + <Insets left="10"/> + </padding> + <HBox fx:id="filteredBox" alignment="CENTER" styleClass="filteredBox" maxHeight="10"> + <ImageView fx:id="filteredImage" fitHeight="20" fitWidth="20"/> + <Label text="FILTERED" styleClass="cell_small_label"> + <padding> + <Insets topRightBottomLeft="5"/> + </padding> + </Label> + </HBox> + </HBox> + </HBox> + <Separator maxWidth="150" styleClass="separator-text"> + <padding> + <Insets top="5" bottom="10"/> + </padding> + </Separator> + <ListView fx:id="personListView" HBox.hgrow="ALWAYS" maxHeight="200" orientation="HORIZONTAL" styleClass="person-list"/> </VBox> diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 58d5ad3dc56..2173e64bf0b 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -5,5 +5,5 @@ <StackPane fx:id="placeHolder" styleClass="pane-with-border" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> - <TextArea fx:id="resultDisplay" editable="false" styleClass="result-display"/> + <TextArea fx:id="resultDisplay" editable="false" styleClass="result-display" wrapText="true"/> </StackPane> diff --git a/src/main/resources/view/WindowAnchorPane.fxml b/src/main/resources/view/WindowAnchorPane.fxml new file mode 100644 index 00000000000..01b1cfccca4 --- /dev/null +++ b/src/main/resources/view/WindowAnchorPane.fxml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.Insets?> +<?import javafx.scene.layout.HBox?> +<?import javafx.scene.layout.VBox?> + +<?import javafx.scene.layout.AnchorPane?> + +<?import javafx.scene.layout.StackPane?> + +<AnchorPane fx:id="container" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" + maxWidth="Infinity" maxHeight="Infinity"> + <VBox fx:id="personList" styleClass="pane-with-border" HBox.hgrow="ALWAYS"> + <padding> + <Insets right="10" left="10" /> + </padding> + <StackPane fx:id="personListPanelPlaceholder" VBox.vgrow="ALWAYS"/> + </VBox> + + <VBox fx:id="inspectionSection" styleClass="pane-with-border" HBox.hgrow="ALWAYS"> + <padding> + <Insets right="10" left="10" /> + </padding> + <StackPane fx:id="inspectionPanelPlaceholder" VBox.vgrow="SOMETIMES"/> + </VBox> + + <VBox fx:id="noteList" styleClass="pane-with-border" HBox.hgrow="ALWAYS"> + <padding> + <Insets right="10" /> + </padding> + <StackPane fx:id="noteListPanelPlaceholder" VBox.vgrow="SOMETIMES"/> + </VBox> +</AnchorPane> diff --git a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json index 6a4d2b7181c..5b39185feb3 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json @@ -3,11 +3,18 @@ "name": "Valid Person", "phone": "9482424", "email": "hans@example.com", - "address": "4th street" + "address": "4th street", + "birthday": "01/01/2000", + "loan" : "0.0", + "history" : [ ] }, { "name": "Person With Invalid Phone Field", "phone": "948asdf2424", "email": "hans@example.com", - "address": "4th street" - } ] + "address": "4th street", + "birthday": "01/01/2000", + "loan": "0.0", + "history" : [ ] + } ], + "tags": [] } diff --git a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json index ccd21f7d1a9..32be916d4e4 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json @@ -3,6 +3,10 @@ "name": "Person with invalid name field: Ha!ns Mu@ster", "phone": "9482424", "email": "hans@example.com", - "address": "4th street" - } ] + "address": "4th street", + "birthday": "01/01/2000", + "loan":"0.0", + "history": [ ] + } ], + "tags": [] } diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json index 48831cc7674..51894bee1a3 100644 --- a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json @@ -4,11 +4,18 @@ "phone": "94351253", "email": "alice@example.com", "address": "123, Jurong West Ave 6, #08-111", - "tagged": [ "friends" ] + "tagged": [ "friends" ], + "birthday": "01/01/2000", + "loan" : "0.0", + "history" : [ ] }, { "name": "Alice Pauline", "phone": "94351253", "email": "pauline@example.com", - "address": "4th street" - } ] + "address": "4th street", + "birthday": "01/01/2000", + "loan": "0.0", + "history" : [ ] + } ], + "tags": [ "friends" ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json index ad3f135ae42..9401f8a26fb 100644 --- a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json @@ -3,6 +3,10 @@ "name": "Hans Muster", "phone": "9482424", "email": "invalid@email!3e", - "address": "4th street" - } ] + "address": "4th street", + "birthday": "01/01/2000", + "loan": "0.0", + "history" : [ ] + } ], + "tags": [] } diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json index f10eddee12e..0a18cfe9912 100644 --- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json @@ -5,42 +5,64 @@ "phone" : "94351253", "email" : "alice@example.com", "address" : "123, Jurong West Ave 6, #08-111", - "tagged" : [ "friends" ] + "birthday": "02/02/2000", + "tagged" : [ "friends" ], + "loan": "0.0", + "history" : [ ] }, { "name" : "Benson Meier", "phone" : "98765432", "email" : "johnd@example.com", "address" : "311, Clementi Ave 2, #02-25", - "tagged" : [ "owesMoney", "friends" ] + "birthday": "01/01/2000", + "tagged" : [ "owesMoney" ], + "loan": "0.0", + "history" : [ ] }, { "name" : "Carl Kurz", "phone" : "95352563", "email" : "heinz@example.com", "address" : "wall street", - "tagged" : [ ] + "birthday": "03/03/2000", + "tagged" : [ ], + "loan": "0.0", + "history" : [ ] }, { "name" : "Daniel Meier", "phone" : "87652533", "email" : "cornelia@example.com", "address" : "10th street", - "tagged" : [ "friends" ] + "birthday": "11/11/2000", + "tagged" : [ "friends" ], + "loan": "0.0", + "history" : [ ] }, { "name" : "Elle Meyer", "phone" : "9482224", "email" : "werner@example.com", "address" : "michegan ave", - "tagged" : [ ] + "birthday": "04/04/2000", + "tagged" : [ ], + "loan": "-10.0", + "history" : [ ] }, { "name" : "Fiona Kunz", "phone" : "9482427", "email" : "lydia@example.com", "address" : "little tokyo", - "tagged" : [ ] + "birthday": "05/05/2000", + "tagged" : [ ], + "loan": "50.0", + "history" : [ ] }, { "name" : "George Best", "phone" : "9482442", "email" : "anna@example.com", "address" : "4th street", - "tagged" : [ ] - } ] + "birthday": "06/06/2000", + "tagged" : [ ], + "loan": "0.0", + "history" : [ ] + } ], + "tags": [ "friends", "owesMoney" ] } diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java index ad923ac249a..c353caeb0d9 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/seedu/address/logic/LogicManagerTest.java @@ -4,6 +4,7 @@ import static seedu.address.commons.core.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.BIRTHDAY_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; @@ -80,7 +81,7 @@ public void execute_storageThrowsIoException_throwsCommandException() { // Execute add command String addCommand = AddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY - + ADDRESS_DESC_AMY; + + ADDRESS_DESC_AMY + BIRTHDAY_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 5865713d5dd..27d269a6a3a 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -1,26 +1,39 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.testutil.Assert.assertThrows; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.function.Predicate; import org.junit.jupiter.api.Test; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; import seedu.address.commons.core.GuiSettings; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.AddCommandParser; +import seedu.address.logic.parser.AddNoteCommandParser; +import seedu.address.logic.parser.CliSyntax; +import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.AddressBook; import seedu.address.model.Model; +import seedu.address.model.ModelManager; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.note.Note; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.NoteBuilder; import seedu.address.testutil.PersonBuilder; public class AddCommandTest { @@ -50,6 +63,85 @@ public void execute_duplicatePerson_throwsCommandException() { assertThrows(CommandException.class, AddCommand.MESSAGE_DUPLICATE_PERSON, () -> addCommand.execute(modelStub)); } + @Test + public void execute_addPersonWithTag_addsTagIntoTagMapping() { + Model model = new ModelManager(); + String tagName = "TagRemovedOnLastPerson"; + String nameA = "personA"; + + assertFalse(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + nameA + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + assertEquals(1, + model.getTagMapping() + .get(tagName) + .getDeepCopiedPersonList() + .stream() + .filter(p -> p.getName().fullName.equals(nameA)) + .count()); + } + + // Ensure that if a Person is added with a Tag that already exists in the address book, the + // Tag that the Person points to is the same object as the Tag that exists in the address book. + @Test + public void execute_addPersonWithTag_tagAlreadyExistsInTagMapping() throws Exception { + Model model = new ModelManager(); + String tagName = "Operations"; + model.addTag(new Tag(tagName)); + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + PersonBuilder.DEFAULT_NAME + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + List<Tag> listOfTagsFromPerson = new ArrayList<>(model.getAddressBook().getPersonList().get(0).getTags()); + Tag tagFromPerson = listOfTagsFromPerson.get(0); + Tag tagFromTagMapping = model.getTagMapping().get(tagName); + + assertSame(tagFromTagMapping, tagFromPerson); + } + + // Tag being added already exists in UniqueTagMapping because a Note has it + @Test + public void execute_addPersonWithTag_tagAlreadyExistsInTagMappingDueToNote() throws Exception { + Model model = new ModelManager(); + String tagName = "Operations"; + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + NoteBuilder.DEFAULT_TITLE + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + PersonBuilder.DEFAULT_NAME + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + List<Tag> listOfTagsFromPerson = new ArrayList<>(model.getAddressBook().getPersonList().get(0).getTags()); + Tag tagFromPerson = listOfTagsFromPerson.get(0); + Tag tagFromTagMapping = model.getTagMapping().get(tagName); + + assertSame(tagFromTagMapping, tagFromPerson); + } + @Test public void equals() { Person alice = new PersonBuilder().withName("Alice").build(); @@ -113,6 +205,11 @@ public void addPerson(Person person) { throw new AssertionError("This method should not be called."); } + @Override + public void addNote(Note note) { + throw new AssertionError("This method should not be called."); + } + @Override public void setAddressBook(ReadOnlyAddressBook newData) { throw new AssertionError("This method should not be called."); @@ -128,25 +225,76 @@ public boolean hasPerson(Person person) { throw new AssertionError("This method should not be called."); } + @Override + public boolean hasNote(Note note) { + throw new AssertionError("This method should not be called."); + } + @Override public void deletePerson(Person target) { throw new AssertionError("This method should not be called."); } + @Override + public void deleteNote(Note target) { + throw new AssertionError("This method should not be called."); + } + @Override public void setPerson(Person target, Person editedPerson) { throw new AssertionError("This method should not be called."); } + @Override + public void setNote(Note target, Note editedNote) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addTag(Tag tag) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void removeTag(Tag tag) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableMap<String, Tag> getTagMapping() { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean notebookContainsTag(Tag tag) { + throw new AssertionError("This method should not be called."); + } + @Override public ObservableList<Person> getFilteredPersonList() { throw new AssertionError("This method should not be called."); } + @Override + public ObservableList<Note> getFilteredNoteList() { + throw new AssertionError("This method should not be called."); + } + @Override public void updateFilteredPersonList(Predicate<Person> predicate) { throw new AssertionError("This method should not be called."); } + + @Override + public void filterPersonListByName(String preamble, String messageUsage, ParseException pe) + throws ParseException { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredNoteList(Predicate<Note> predicate) { + throw new AssertionError("This method should not be called."); + } } /** @@ -185,6 +333,11 @@ public void addPerson(Person person) { personsAdded.add(person); } + @Override + public ObservableMap<String, Tag> getTagMapping() { + return FXCollections.observableHashMap(); + } + @Override public ReadOnlyAddressBook getAddressBook() { return new AddressBook(); diff --git a/src/test/java/seedu/address/logic/commands/AddNoteCommandTest.java b/src/test/java/seedu/address/logic/commands/AddNoteCommandTest.java new file mode 100644 index 00000000000..ad053168ca7 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/AddNoteCommandTest.java @@ -0,0 +1,336 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.AddCommandParser; +import seedu.address.logic.parser.AddNoteCommandParser; +import seedu.address.logic.parser.CliSyntax; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.note.Note; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.NoteBuilder; +import seedu.address.testutil.PersonBuilder; + +public class AddNoteCommandTest { + + @Test + public void constructor_nullNote_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new AddNoteCommand(null)); + } + + @Test + public void execute_noteAcceptedByModel_addSuccessful() throws Exception { + ModelStubAcceptingNoteAdded modelStub = new ModelStubAcceptingNoteAdded(); + Note validNote = new NoteBuilder().build(); + + CommandResult commandResult = new AddNoteCommand(validNote).execute(modelStub); + + assertEquals(String.format(AddNoteCommand.MESSAGE_SUCCESS, validNote), commandResult.getFeedbackToUser()); + assertEquals(Arrays.asList(validNote), modelStub.notesAdded); + + } + + @Test + public void execute_duplicateNote_throwsCommandException() { + Note validNote = new NoteBuilder().build(); + AddNoteCommand addNoteCommand = new AddNoteCommand(validNote); + ModelStub modelStub = new ModelStubWithNote(validNote); + + assertThrows(CommandException.class, AddNoteCommand.MESSAGE_DUPLICATE_NOTE, () -> + addNoteCommand.execute(modelStub)); + } + + @Test + public void execute_addNoteWithTag_addsTagIntoTagMapping() { + Model model = new ModelManager(); + String tagName = "Operations"; + + assertFalse(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + NoteBuilder.DEFAULT_TITLE + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + } + + // Ensure that if a Note is added with a Tag that already exists in the address book, the + // Tag that the Note points to is the same object as the Tag that exists in the address book. + @Test + public void execute_addNoteWithTag_tagAlreadyExistsInTagMapping() throws Exception { + Model model = new ModelManager(); + String tagName = "Operations"; + model.addTag(new Tag(tagName)); + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + NoteBuilder.DEFAULT_TITLE + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + List<Tag> listOfTagsFromNote = new ArrayList<>(model.getAddressBook().getNoteBook().get(0).getTags()); + Tag tagFromNote = listOfTagsFromNote.get(0); + Tag tagFromTagMapping = model.getTagMapping().get(tagName); + + assertSame(tagFromTagMapping, tagFromNote); + } + + // Tag being added already exists in UniqueTagMapping because a Person has it + @Test + public void execute_addNoteWithTag_tagAlreadyExistsInTagMappingDueToPerson() throws Exception { + Model model = new ModelManager(); + String tagName = "Operations"; + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + PersonBuilder.DEFAULT_NAME + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + NoteBuilder.DEFAULT_TITLE + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + List<Tag> listOfTagsFromNotes = new ArrayList<>(model.getAddressBook().getNoteBook().get(0).getTags()); + Tag tagFromNotes = listOfTagsFromNotes.get(0); + Tag tagFromTagMapping = model.getTagMapping().get(tagName); + + assertSame(tagFromTagMapping, tagFromNotes); + } + + @Test + public void equals() { + Note meeting = new NoteBuilder().withTitle("Meeting").build(); + Note event = new NoteBuilder().withTitle("Event").build(); + AddNoteCommand addMeetingNoteCommand = new AddNoteCommand(meeting); + AddNoteCommand addEventNoteCommand = new AddNoteCommand(event); + + + // same object -> returns true + assertTrue(addMeetingNoteCommand.equals(addMeetingNoteCommand)); + + // same values -> returns true + AddNoteCommand addMeetingNoteCommandCopy = new AddNoteCommand(meeting); + assertTrue(addMeetingNoteCommand.equals(addMeetingNoteCommandCopy)); + + // different types -> returns false + assertFalse(addMeetingNoteCommand.equals(1)); + + // null -> returns false + assertFalse(addMeetingNoteCommand.equals(null)); + + // different person -> returns false + assertFalse(addMeetingNoteCommand.equals(addEventNoteCommand)); + } + + /** + * A default model stub that have all of the methods failing. + */ + private class ModelStub implements Model { + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + throw new AssertionError("This method should not be called."); + } + + @Override + public GuiSettings getGuiSettings() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Path getAddressBookFilePath() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBookFilePath(Path addressBookFilePath) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addNote(Note note) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setAddressBook(ReadOnlyAddressBook newData) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean hasPerson(Person person) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean hasNote(Note note) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deletePerson(Person target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteNote(Note target) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setPerson(Person target, Person editedPerson) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setNote(Note target, Note editedNote) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addTag(Tag tag) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void removeTag(Tag tag) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableMap<String, Tag> getTagMapping() { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean notebookContainsTag(Tag tag) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList<Person> getFilteredPersonList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObservableList<Note> getFilteredNoteList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredPersonList(Predicate<Person> predicate) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void filterPersonListByName(String preamble, String messageUsage, ParseException pe) + throws ParseException { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredNoteList(Predicate<Note> predicate) { + throw new AssertionError("This method should not be called."); + } + } + + /** + * A Model stub that contains a single existing note. + */ + private class ModelStubWithNote extends ModelStub { + private final Note note; + + ModelStubWithNote(Note note) { + requireNonNull(note); + this.note = note; + } + + @Override + public boolean hasNote(Note note) { + requireNonNull(note); + return this.note.isSameNote(note); + } + } + + /** + * A Model stub that always accept the note being added. + */ + private class ModelStubAcceptingNoteAdded extends ModelStub { + final ArrayList<Note> notesAdded = new ArrayList<>(); + + @Override + public boolean hasNote(Note note) { + requireNonNull(note); + return notesAdded.stream().anyMatch(note::isSameNote); + } + + @Override + public void addNote(Note note) { + requireNonNull(note); + notesAdded.add(note); + } + + @Override + public ObservableMap<String, Tag> getTagMapping() { + return FXCollections.observableHashMap(); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + return new AddressBook(); + } + } + +} diff --git a/src/test/java/seedu/address/logic/commands/CommandResultTest.java b/src/test/java/seedu/address/logic/commands/CommandResultTest.java index 4f3eb46e9ef..fa4c3af5969 100644 --- a/src/test/java/seedu/address/logic/commands/CommandResultTest.java +++ b/src/test/java/seedu/address/logic/commands/CommandResultTest.java @@ -14,7 +14,7 @@ public void equals() { // same values -> returns true assertTrue(commandResult.equals(new CommandResult("feedback"))); - assertTrue(commandResult.equals(new CommandResult("feedback", false, false))); + assertTrue(commandResult.equals(new CommandResult("feedback", CommandResult.UiState.None))); // same object -> returns true assertTrue(commandResult.equals(commandResult)); @@ -29,10 +29,10 @@ public void equals() { assertFalse(commandResult.equals(new CommandResult("different"))); // different showHelp value -> returns false - assertFalse(commandResult.equals(new CommandResult("feedback", true, false))); + assertFalse(commandResult.equals(new CommandResult("feedback", CommandResult.UiState.ShowHelp))); // different exit value -> returns false - assertFalse(commandResult.equals(new CommandResult("feedback", false, true))); + assertFalse(commandResult.equals(new CommandResult("feedback", CommandResult.UiState.Exit))); } @Test @@ -46,9 +46,11 @@ public void hashcode() { assertNotEquals(commandResult.hashCode(), new CommandResult("different").hashCode()); // different showHelp value -> returns different hashcode - assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", true, false).hashCode()); + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", + CommandResult.UiState.ShowHelp).hashCode()); // different exit value -> returns different hashcode - assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false, true).hashCode()); + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", + CommandResult.UiState.Exit).hashCode()); } } diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java index 643a1d08069..3b25eb23cc3 100644 --- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java @@ -3,7 +3,10 @@ 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_BIRTHDAY; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LOAN_AMOUNT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LOAN_REASON; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -17,8 +20,11 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.AddressBook; import seedu.address.model.Model; +import seedu.address.model.note.Note; +import seedu.address.model.note.TitleContainsKeywordsPredicate; import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; +import seedu.address.testutil.EditNoteDescriptorBuilder; import seedu.address.testutil.EditPersonDescriptorBuilder; /** @@ -34,8 +40,18 @@ 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_BIRTHDAY_AMY = "01/01/2000"; + public static final String VALID_BIRTHDAY_BOB = "01/01/2000"; public static final String VALID_TAG_HUSBAND = "husband"; public static final String VALID_TAG_FRIEND = "friend"; + public static final String VALID_AMOUNT = "2.55"; + public static final String VALID_REASON = "Testcase"; + + public static final String VALID_TITLE_MEETING = "Meeting"; + public static final String VALID_TITLE_CLUB = "Club"; + public static final String VALID_CONTENT_MEETING = "October 3rd"; + public static final String VALID_CONTENT_CLUB = "Birthday celebration"; + public static final String VALID_TAG_IMPORTANT = "important"; public static final String NAME_DESC_AMY = " " + PREFIX_NAME + VALID_NAME_AMY; public static final String NAME_DESC_BOB = " " + PREFIX_NAME + VALID_NAME_BOB; @@ -45,6 +61,11 @@ 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 BIRTHDAY_DESC_AMY = " " + PREFIX_BIRTHDAY + VALID_BIRTHDAY_AMY; + public static final String BIRTHDAY_DESC_BOB = " " + PREFIX_BIRTHDAY + VALID_BIRTHDAY_BOB; + + public static final String AMOUNT_DESC = " " + PREFIX_LOAN_AMOUNT + VALID_AMOUNT; + public static final String REASON_DESC = " " + PREFIX_LOAN_REASON + VALID_REASON; public static final String TAG_DESC_FRIEND = " " + PREFIX_TAG + VALID_TAG_FRIEND; public static final String TAG_DESC_HUSBAND = " " + PREFIX_TAG + VALID_TAG_HUSBAND; @@ -52,21 +73,33 @@ public class CommandTestUtil { 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_BIRTHDAY_DESC = " " + PREFIX_BIRTHDAY + "asd"; + public static final String INVALID_AMOUNT_DESC = " " + PREFIX_LOAN_AMOUNT + "7.555"; // too many decimal places + public static final String INVALID_REASON_DESC = " " + PREFIX_LOAN_REASON + ""; // cannot be empty public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags public static final String PREAMBLE_WHITESPACE = "\t \r \n"; public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; + public static final String INVALID_NON_MATCHING_NOTE_TITLE = "abcdefghijklmnop"; + public static final EditCommand.EditPersonDescriptor DESC_AMY; public static final EditCommand.EditPersonDescriptor DESC_BOB; + public static final EditNoteCommand.EditNoteDescriptor DESC_MEETING; + public static final EditNoteCommand.EditNoteDescriptor DESC_CLUB; + 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(); + .withBirthday(VALID_BIRTHDAY_AMY).withTags(VALID_TAG_FRIEND).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(); + .withBirthday(VALID_BIRTHDAY_BOB).withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); + DESC_MEETING = new EditNoteDescriptorBuilder().withTitle(VALID_TITLE_MEETING) + .withContent(VALID_CONTENT_MEETING).withTags(VALID_TAG_IMPORTANT).build(); + DESC_CLUB = new EditNoteDescriptorBuilder().withTitle(VALID_TITLE_CLUB) + .withContent(VALID_CONTENT_CLUB).withTags(VALID_TAG_IMPORTANT).build(); } /** @@ -99,17 +132,19 @@ public static void assertCommandSuccess(Command command, Model actualModel, Stri * Executes the given {@code command}, confirms that <br> * - a {@code CommandException} is thrown <br> * - the CommandException message matches {@code expectedMessage} <br> - * - the address book, filtered person list and selected person in {@code actualModel} remain unchanged + * - the address book, filtered person & note list, selected person & note in {@code actualModel} remain unchanged */ public static void assertCommandFailure(Command command, Model actualModel, String expectedMessage) { // we are unable to defensively copy the model for comparison later, so we can // only do so by copying its components. AddressBook expectedAddressBook = new AddressBook(actualModel.getAddressBook()); List<Person> expectedFilteredList = new ArrayList<>(actualModel.getFilteredPersonList()); + List<Note> expectedFilteredNoteList = new ArrayList<>(actualModel.getFilteredNoteList()); assertThrows(CommandException.class, expectedMessage, () -> command.execute(actualModel)); assertEquals(expectedAddressBook, actualModel.getAddressBook()); assertEquals(expectedFilteredList, actualModel.getFilteredPersonList()); + assertEquals(expectedFilteredNoteList, actualModel.getFilteredNoteList()); } /** * Updates {@code model}'s filtered list to show only the person at the given {@code targetIndex} in the @@ -125,4 +160,18 @@ public static void showPersonAtIndex(Model model, Index targetIndex) { assertEquals(1, model.getFilteredPersonList().size()); } + /** + * Updates {@code model}'s filtered note list to show only the note at the given {@code targetIndex} in the + * {@code model}'s address book. + */ + public static void showNoteAtIndex(Model model, Index targetIndex) { + assertTrue(targetIndex.getZeroBased() < model.getFilteredNoteList().size()); + + Note note = model.getFilteredNoteList().get(targetIndex.getZeroBased()); + final String[] splitTitle = note.getTitle().fullTitle.split("\\s+"); + model.updateFilteredNoteList(new TitleContainsKeywordsPredicate(Arrays.asList(splitTitle[0]))); + + assertEquals(1, model.getFilteredNoteList().size()); + } + } diff --git a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java index 45a8c910ba1..c44e1b108ee 100644 --- a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java @@ -1,5 +1,6 @@ package seedu.address.logic.commands; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; @@ -13,10 +14,16 @@ import seedu.address.commons.core.Messages; import seedu.address.commons.core.index.Index; +import seedu.address.logic.parser.AddCommandParser; +import seedu.address.logic.parser.AddNoteCommandParser; +import seedu.address.logic.parser.CliSyntax; +import seedu.address.logic.parser.DeleteCommandParser; 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.NoteBuilder; +import seedu.address.testutil.PersonBuilder; /** * Contains integration tests (interaction with the Model) and unit tests for @@ -106,4 +113,68 @@ private void showNoPerson(Model model) { assertTrue(model.getFilteredPersonList().isEmpty()); } + + @Test + public void execute_removeLastPersonFromModel_removesTagInMapping() { + Model model = new ModelManager(); + String tagName = "TagRemovedOnLastPerson"; + String nameA = "personA"; + String nameB = "personB"; + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + nameA + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + nameB + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertAll(() -> new DeleteCommandParser(model).parse(nameB).execute(model)); + assertTrue(model.getTagMapping().containsKey(tagName)); + assertAll(() -> new DeleteCommandParser(model).parse(nameA).execute(model)); + + assertFalse(model.getTagMapping().containsKey(tagName)); + } + + // Tag is not deleted from UniqueTagMapping when there still exists a Note with that Tag + @Test + public void execute_deletePersonWithTag_doesNotDeletesTagFromTagMapping() { + Model model = new ModelManager(); + String tagName = "Operations"; + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + NoteBuilder.DEFAULT_TITLE + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + PersonBuilder.DEFAULT_NAME + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new DeleteCommandParser(model).parse(" " + + "1") + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + } } diff --git a/src/test/java/seedu/address/logic/commands/DeleteNoteCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteNoteCommandTest.java new file mode 100644 index 00000000000..4433fbc4fe4 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteNoteCommandTest.java @@ -0,0 +1,172 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertAll; +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.showNoteAtIndex; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_NOTE; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_NOTE; +import static seedu.address.testutil.TypicalNotes.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.parser.AddCommandParser; +import seedu.address.logic.parser.AddNoteCommandParser; +import seedu.address.logic.parser.CliSyntax; +import seedu.address.logic.parser.DeleteNoteCommandParser; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.note.Note; +import seedu.address.testutil.NoteBuilder; +import seedu.address.testutil.PersonBuilder; + +public class DeleteNoteCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_validIndexUnfilteredList_success() { + Note noteToDelete = model.getFilteredNoteList().get(INDEX_FIRST_NOTE.getZeroBased()); + DeleteNoteCommand deleteNoteCommand = new DeleteNoteCommand(INDEX_FIRST_NOTE); + + String expectedMessage = String.format(DeleteNoteCommand.MESSAGE_DELETE_NOTE_SUCCESS, noteToDelete); + + ModelManager expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.deleteNote(noteToDelete); + + assertCommandSuccess(deleteNoteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexUnfilteredList_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredNoteList().size() + 1); + DeleteNoteCommand deleteNoteCommand = new DeleteNoteCommand(outOfBoundIndex); + + assertCommandFailure(deleteNoteCommand, model, Messages.MESSAGE_INVALID_NOTE_DISPLAYED_INDEX); + } + + @Test + public void execute_validIndexFilteredList_success() { + showNoteAtIndex(model, INDEX_FIRST_NOTE); + + Note noteToDelete = model.getFilteredNoteList().get(INDEX_FIRST_NOTE.getZeroBased()); + DeleteNoteCommand deleteNoteCommand = new DeleteNoteCommand(INDEX_FIRST_NOTE); + + String expectedMessage = String.format(DeleteNoteCommand.MESSAGE_DELETE_NOTE_SUCCESS, noteToDelete); + + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + expectedModel.deleteNote(noteToDelete); + showNoNote(expectedModel); + + assertCommandSuccess(deleteNoteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidIndexFilteredList_throwsCommandException() { + showNoteAtIndex(model, INDEX_FIRST_NOTE); + + Index outOfBoundIndex = INDEX_SECOND_NOTE; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getNoteBook().size()); + + DeleteNoteCommand deleteNoteCommand = new DeleteNoteCommand(outOfBoundIndex); + + assertCommandFailure(deleteNoteCommand, model, Messages.MESSAGE_INVALID_NOTE_DISPLAYED_INDEX); + } + + @Test + public void equals() { + DeleteNoteCommand deleteFirstNoteCommand = new DeleteNoteCommand(INDEX_FIRST_NOTE); + DeleteNoteCommand deleteSecondNoteCommand = new DeleteNoteCommand(INDEX_SECOND_NOTE); + + // same object -> returns true + assertTrue(deleteFirstNoteCommand.equals(deleteFirstNoteCommand)); + + // same values -> returns true + DeleteNoteCommand deleteFirstNoteCommandCopy = new DeleteNoteCommand(INDEX_FIRST_NOTE); + assertTrue(deleteFirstNoteCommand.equals(deleteFirstNoteCommandCopy)); + + // different types -> returns false + assertFalse(deleteFirstNoteCommand.equals(1)); + + // null -> returns false + assertFalse(deleteFirstNoteCommand.equals(null)); + + // different person -> returns false + assertFalse(deleteFirstNoteCommand.equals(deleteSecondNoteCommand)); + } + + + /** + * Updates {@code model}'s filtered note list to show no notes. + */ + private void showNoNote(Model model) { + model.updateFilteredNoteList(n -> false); + + assertTrue(model.getFilteredNoteList().isEmpty()); + } + + + @Test + public void execute_removeLastNoteFromModel_removesTagInMapping() { + Model model = new ModelManager(); + String tagName = "TagRemovedOnLastNote"; + String titleA = "noteA"; + String titleB = "noteB"; + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + titleA + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + titleB + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertAll(() -> new DeleteNoteCommandParser().parse("2").execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new DeleteNoteCommandParser().parse("1").execute(model)); + + assertFalse(model.getTagMapping().containsKey(tagName)); + } + + // Tag is not deleted from UniqueTagMapping when there still exists a Person with that Tag + @Test + public void execute_deleteNoteWithTag_doesNotDeletesTagFromTagMapping() { + Model model = new ModelManager(); + String tagName = "Operations"; + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + PersonBuilder.DEFAULT_NAME + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + NoteBuilder.DEFAULT_TITLE + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new DeleteNoteCommandParser().parse("1").execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java index 214c6c2507b..c3281703bb4 100644 --- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java @@ -1,6 +1,9 @@ package seedu.address.logic.commands; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.commands.CommandTestUtil.DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.DESC_BOB; @@ -14,17 +17,26 @@ import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; +import java.util.ArrayList; +import java.util.List; + import org.junit.jupiter.api.Test; import seedu.address.commons.core.Messages; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.parser.AddCommandParser; +import seedu.address.logic.parser.AddNoteCommandParser; +import seedu.address.logic.parser.CliSyntax; +import seedu.address.logic.parser.EditCommandParser; 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.model.tag.Tag; import seedu.address.testutil.EditPersonDescriptorBuilder; +import seedu.address.testutil.NoteBuilder; import seedu.address.testutil.PersonBuilder; /** @@ -145,6 +157,131 @@ public void execute_invalidPersonIndexFilteredList_failure() { assertCommandFailure(editCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } + @Test + public void execute_editPersonWithTag_addsTagIntoTagMapping() throws Exception { + Model model = new ModelManager(); + String tagName = "Operations"; + model.addPerson(new PersonBuilder().build()); + + assertFalse(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new EditCommandParser(model).parse(" " + + "1" + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + + assertEquals(1, + model.getTagMapping() + .get(tagName) + .getDeepCopiedPersonList() + .stream() + .filter(p -> p.getName().fullName.equals(PersonBuilder.DEFAULT_NAME)) + .count()); + } + + // Ensure that if a Person is edited with a Tag that already exists in the address book, the + // Tag that the Person points to is the same object as the Tag that exists in the address book. + @Test + public void execute_editPersonWithTag_tagAlreadyExistsInTagMapping() throws Exception { + Model model = new ModelManager(); + String tagName = "Operations"; + model.addPerson(new PersonBuilder().build()); + model.addTag(new Tag(tagName)); + + assertAll(() -> new EditCommandParser(model).parse(" " + + "1" + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + List<Tag> listOfTagsFromPerson = new ArrayList<>(model.getAddressBook().getPersonList().get(0).getTags()); + Tag tagFromPerson = listOfTagsFromPerson.get(0); + Tag tagFromTagMapping = model.getTagMapping().get(tagName); + + assertSame(tagFromTagMapping, tagFromPerson); + } + + @Test + public void execute_editPersonWithTag_deletesTagFromTagMapping() { + Model model = new ModelManager(); + String tagName = "Operations"; + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + PersonBuilder.DEFAULT_NAME + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new EditCommandParser(model).parse(" " + + "1" + " " + + CliSyntax.PREFIX_TAG + " ") + .execute(model)); + + assertFalse(model.getTagMapping().containsKey(tagName)); + } + + // Tag being added already exists in UniqueTagMapping because a Note has it + @Test + public void execute_editPersonWithTag_tagAlreadyExistsInTagMappingDueToNote() throws Exception { + Model model = new ModelManager(); + model.addPerson(new PersonBuilder().build()); + String tagName = "Operations"; + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + NoteBuilder.DEFAULT_TITLE + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertAll(() -> new EditCommandParser(model).parse(" " + + "1" + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + List<Tag> listOfTagsFromPerson = new ArrayList<>(model.getAddressBook().getPersonList().get(0).getTags()); + Tag tagFromPerson = listOfTagsFromPerson.get(0); + Tag tagFromTagMapping = model.getTagMapping().get(tagName); + + assertSame(tagFromTagMapping, tagFromPerson); + } + + // Tag is not deleted from UniqueTagMapping when there still exists a Note with that Tag + @Test + public void execute_editPersonWithTag_doesNotDeletesTagFromTagMapping() { + Model model = new ModelManager(); + String tagName = "Operations"; + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + NoteBuilder.DEFAULT_TITLE + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + PersonBuilder.DEFAULT_NAME + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new EditCommandParser(model).parse(" " + + "1" + " " + + CliSyntax.PREFIX_TAG + " ") + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + } + @Test public void equals() { final EditCommand standardCommand = new EditCommand(INDEX_FIRST_PERSON, DESC_AMY); diff --git a/src/test/java/seedu/address/logic/commands/EditLoanCommandTest.java b/src/test/java/seedu/address/logic/commands/EditLoanCommandTest.java new file mode 100644 index 00000000000..992744e0b02 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/EditLoanCommandTest.java @@ -0,0 +1,245 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +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.EditLoanCommand.EditLoanDescriptor; +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.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +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.Loan; +import seedu.address.model.person.LoanHistory; +import seedu.address.model.person.Person; +import seedu.address.model.person.Reason; +import seedu.address.model.person.exceptions.LoanOutOfBoundsException; + +/** + * Contains integration tests (interaction with the Model) and unit tests for EditCommand. + */ +public class EditLoanCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_unfilteredList_success() { + Loan initialLoan = model.getAddressBook().getPersonList().get(0).getLoan(); + Loan changedLoan = new Loan(initialLoan.getAmount() + 5.55); + + LoanHistory addedLoan = new LoanHistory( + new Loan(5.55), + new Reason("Test") + ); + + EditLoanDescriptor descriptor = + new EditLoanDescriptor(changedLoan, addedLoan); + + EditLoanCommand editLoanCommand = + new EditLoanCommand(INDEX_FIRST_PERSON, descriptor); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + Person initialPerson = model.getFilteredPersonList().get(0); + List<LoanHistory> newHistory = new ArrayList<>(initialPerson.getHistory()); + descriptor.getHistory().ifPresent(newHistory::add); + + Person editedPerson = new Person( + initialPerson.getName(), + initialPerson.getPhone(), + initialPerson.getEmail(), + initialPerson.getAddress(), + initialPerson.getBirthday(), + initialPerson.getTags(), + descriptor.getLoan().orElseThrow(() -> new NullPointerException("No loan found!")), + newHistory); + + expectedModel.setPerson(initialPerson, editedPerson); + + CommandResult expectedResult = new CommandResult( + String.format(EditLoanCommand.MESSAGE_EDIT_LOAN_SUCCESS, + expectedModel.getAddressBook().getPersonList().get(0)), + CommandResult.UiState.Inspect, "1"); + + assertCommandSuccess(editLoanCommand, model, expectedResult, expectedModel); + } + + + @Test + public void execute_filteredList_success() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + + Person personInFilteredList = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + + Loan initialLoan = personInFilteredList.getLoan(); + Loan incrementLoan = new Loan("-$31.66"); + Loan changedLoan; + + try { + changedLoan = initialLoan.addBy(incrementLoan); + } catch (LoanOutOfBoundsException e) { + fail(); + return; + } + + EditLoanDescriptor editLoanDescriptor = new EditLoanDescriptor(changedLoan, + new LoanHistory(incrementLoan, new Reason("Testing"))); + + List<LoanHistory> loanHistoryList = personInFilteredList.getHistory(); + editLoanDescriptor.getHistory().ifPresent(loanHistoryList::add); + + + Person editedPerson = new Person( + personInFilteredList.getName(), personInFilteredList.getPhone(), + personInFilteredList.getEmail(), personInFilteredList.getAddress(), + personInFilteredList.getBirthday(), personInFilteredList.getTags(), + editLoanDescriptor.getLoan().orElseThrow(() -> + new NullPointerException("There was no loan")), + loanHistoryList + ); + + EditLoanCommand editLoanCommand = new EditLoanCommand(INDEX_FIRST_PERSON, editLoanDescriptor); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + + expectedModel.setPerson(personInFilteredList, editedPerson); + + CommandResult expectedResult = new CommandResult( + String.format(EditLoanCommand.MESSAGE_EDIT_LOAN_SUCCESS, + expectedModel.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased())), + CommandResult.UiState.Inspect, "1"); + + assertCommandSuccess(editLoanCommand, model, expectedResult, expectedModel); + } + + + @Test + public void execute_invalidPersonIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + EditLoanDescriptor descriptor = new EditLoanDescriptor(new Loan(0), + new LoanHistory(new Loan(0), new Reason("This does not matter"))); + EditLoanCommand editCommand = new EditLoanCommand(outOfBoundIndex, descriptor); + + assertCommandFailure(editCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + /** + * Checks that a value too large causes an LoanOutOfBounds exception caught + */ + @Test + public void execute_loanTooLarge_throwsOutOfBounds() { + Loan incrementLoan = new Loan("500000000000.01"); + + LoanHistory loanHistory = new LoanHistory( + incrementLoan, + new Reason("Test") + ); + + EditLoanDescriptor descriptor = + new EditLoanDescriptor(incrementLoan, loanHistory); + + EditLoanCommand editLoanCommand = + new EditLoanCommand(INDEX_FIRST_PERSON, descriptor); + + try { + editLoanCommand.execute(model); + } catch (CommandException e) { + fail(); + } + + assertCommandFailure(editLoanCommand, model, EditLoanCommand.OUT_OF_BOUNDS_NOTIFICATION); + } + + /** + * Checks that a value too small causes an LoanOutOfBounds exception caught + */ + @Test + public void execute_loanTooSmall_throwsOutOfBounds() { + Loan incrementLoan = new Loan("-500000000000.01"); + + LoanHistory loanHistory = new LoanHistory( + incrementLoan, + new Reason("Test") + ); + + EditLoanDescriptor descriptor = + new EditLoanDescriptor(incrementLoan, loanHistory); + + EditLoanCommand editLoanCommand = + new EditLoanCommand(INDEX_FIRST_PERSON, descriptor); + + try { + editLoanCommand.execute(model); + } catch (CommandException e) { + fail(); + } + + assertCommandFailure(editLoanCommand, model, EditLoanCommand.OUT_OF_BOUNDS_NOTIFICATION); + } + + /** + * Edit filtered list where index is larger than size of filtered list, + * but smaller than size of address book + */ + @Test + public void execute_invalidPersonIndexFilteredList_failure() { + 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()); + + EditLoanCommand editLoanCommand = new EditLoanCommand(outOfBoundIndex, + new EditLoanDescriptor(new Loan(0), + new LoanHistory(new Loan(0), new Reason("This does not matter")))); + + assertCommandFailure(editLoanCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void equals() { + final EditLoanDescriptor editLoanDescriptor = new EditLoanDescriptor(new Loan(355.13), + new LoanHistory(new Loan(55.13), new Reason("Special"))); + final EditLoanCommand standardCommand = new EditLoanCommand(INDEX_FIRST_PERSON, + editLoanDescriptor); + + // same values -> returns true + EditLoanDescriptor copyDescriptor = new EditLoanDescriptor(editLoanDescriptor); + EditLoanCommand commandWithSameValues = new EditLoanCommand(INDEX_FIRST_PERSON, copyDescriptor); + assertEquals(standardCommand, commandWithSameValues); + + // same object -> returns true + assertEquals(standardCommand, standardCommand); + + // null -> returns false + assertNotEquals(null, standardCommand); + + // different types -> returns false + assertNotEquals(standardCommand, new ClearCommand()); + + // different index -> returns false + assertNotEquals(standardCommand, new EditLoanCommand(INDEX_SECOND_PERSON, editLoanDescriptor)); + + // different descriptor -> returns false + assertNotEquals(standardCommand, new EditLoanCommand(INDEX_FIRST_PERSON, + new EditLoanDescriptor(new Loan("50"), + new LoanHistory(new Loan("1"), new Reason("Different descriptor") + ) + )) + ); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/EditNoteCommandTest.java b/src/test/java/seedu/address/logic/commands/EditNoteCommandTest.java new file mode 100644 index 00000000000..8f86c1fd97c --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/EditNoteCommandTest.java @@ -0,0 +1,302 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.DESC_CLUB; +import static seedu.address.logic.commands.CommandTestUtil.DESC_MEETING; +import static seedu.address.logic.commands.CommandTestUtil.VALID_CONTENT_MEETING; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TITLE_CLUB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TITLE_MEETING; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showNoteAtIndex; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_NOTE; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_NOTE; +import static seedu.address.testutil.TypicalNotes.getTypicalAddressBook; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditNoteCommand.EditNoteDescriptor; +import seedu.address.logic.parser.AddCommandParser; +import seedu.address.logic.parser.AddNoteCommandParser; +import seedu.address.logic.parser.CliSyntax; +import seedu.address.logic.parser.EditNoteCommandParser; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.note.Note; +import seedu.address.model.tag.Tag; +import seedu.address.testutil.EditNoteDescriptorBuilder; +import seedu.address.testutil.NoteBuilder; +import seedu.address.testutil.PersonBuilder; + +/** + * Contains integration tests (interaction with the Model) and unit tests for EditNoteCommand. + */ +public class EditNoteCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_allFieldsSpecifiedUnfilteredList_success() { + Note editedNote = new NoteBuilder().build(); + EditNoteDescriptor descriptor = new EditNoteDescriptorBuilder(editedNote).build(); + EditNoteCommand editNoteCommand = new EditNoteCommand(INDEX_FIRST_NOTE, descriptor); + + String expectedMessage = String.format(EditNoteCommand.MESSAGE_EDIT_NOTE_SUCCESS, editedNote); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setNote(model.getFilteredNoteList().get(0), editedNote); + + assertCommandSuccess(editNoteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_someFieldsSpecifiedUnfilteredList_success() { + Index indexLastNote = Index.fromOneBased(model.getFilteredNoteList().size()); + Note lastNote = model.getFilteredNoteList().get(indexLastNote.getZeroBased()); + + NoteBuilder noteInList = new NoteBuilder(lastNote); + Note editedNote = noteInList.withTitle(VALID_TITLE_MEETING).withContent(VALID_CONTENT_MEETING).build(); + + EditNoteCommand.EditNoteDescriptor descriptor = new EditNoteDescriptorBuilder().withTitle(VALID_TITLE_MEETING) + .withContent(VALID_CONTENT_MEETING).build(); + + EditNoteCommand editNoteCommand = new EditNoteCommand(indexLastNote, descriptor); + + String expectedMessage = String.format(EditNoteCommand.MESSAGE_EDIT_NOTE_SUCCESS, editedNote); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setNote(lastNote, editedNote); + + assertCommandSuccess(editNoteCommand, model, expectedMessage, expectedModel); + + } + + @Test + public void execute_noFieldSpecifiedUnfilteredList_success() { + EditNoteCommand editNoteCommand = new EditNoteCommand(INDEX_FIRST_NOTE, new EditNoteDescriptor()); + Note editedNote = model.getFilteredNoteList().get(INDEX_FIRST_NOTE.getZeroBased()); + + String expectedMessage = String.format(EditNoteCommand.MESSAGE_EDIT_NOTE_SUCCESS, editedNote); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + + assertCommandSuccess(editNoteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_filteredList_success() { + showNoteAtIndex(model, INDEX_FIRST_NOTE); + + Note noteInFilteredList = model.getFilteredNoteList().get(INDEX_FIRST_NOTE.getZeroBased()); + Note editedNote = new NoteBuilder(noteInFilteredList).withTitle(VALID_TITLE_CLUB).build(); + EditNoteCommand editNoteCommand = new EditNoteCommand(INDEX_FIRST_NOTE, + new EditNoteDescriptorBuilder().withTitle(VALID_TITLE_CLUB).build()); + + String expectedMessage = String.format(EditNoteCommand.MESSAGE_EDIT_NOTE_SUCCESS, editedNote); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setNote(model.getFilteredNoteList().get(0), editedNote); + + assertCommandSuccess(editNoteCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_duplicateNoteUnfilteredList_failure() { + Note firstNote = model.getFilteredNoteList().get(INDEX_FIRST_NOTE.getZeroBased()); + EditNoteDescriptor descriptor = new EditNoteDescriptorBuilder(firstNote).build(); + EditNoteCommand editNoteCommand = new EditNoteCommand(INDEX_SECOND_NOTE, descriptor); + + assertCommandFailure(editNoteCommand, model, EditNoteCommand.MESSAGE_DUPLICATE_NOTE); + } + + @Test + public void execute_duplicateNoteFilteredList_failure() { + showNoteAtIndex(model, INDEX_FIRST_NOTE); + + // edit note in filtered note list into a duplicate in address book + Note noteInList = model.getAddressBook().getNoteBook().get(INDEX_SECOND_NOTE.getZeroBased()); + EditNoteCommand editNoteCommand = new EditNoteCommand(INDEX_FIRST_NOTE, + new EditNoteDescriptorBuilder(noteInList).build()); + + assertCommandFailure(editNoteCommand, model, EditNoteCommand.MESSAGE_DUPLICATE_NOTE); + } + + @Test + public void execute_invalidNoteIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredNoteList().size() + 1); + EditNoteDescriptor descriptor = new EditNoteDescriptorBuilder().withTitle(VALID_TITLE_CLUB).build(); + EditNoteCommand editNoteCommand = new EditNoteCommand(outOfBoundIndex, descriptor); + + assertCommandFailure(editNoteCommand, model, Messages.MESSAGE_INVALID_NOTE_DISPLAYED_INDEX); + } + + /** + * Edit filtered note list where index is larger than size of filtered list, + * but smaller than size of address book + */ + @Test + public void execute_invalidNoteIndexFilteredList_failure() { + showNoteAtIndex(model, INDEX_FIRST_NOTE); + Index outOfBoundIndex = INDEX_SECOND_NOTE; + // ensures that outOfBoundIndex is still in bounds of address book note list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getNoteBook().size()); + + EditNoteCommand editNoteCommand = new EditNoteCommand(outOfBoundIndex, + new EditNoteDescriptorBuilder().withTitle(VALID_TITLE_CLUB).build()); + + assertCommandFailure(editNoteCommand, model, Messages.MESSAGE_INVALID_NOTE_DISPLAYED_INDEX); + } + + @Test + public void execute_editNoteWithTag_addsTagIntoTagMapping() throws Exception { + Model model = new ModelManager(); + String tagName = "Operations"; + model.addNote(new NoteBuilder().build()); + + assertFalse(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new EditNoteCommandParser(model).parse(" " + + "1" + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + } + + // Ensure that if a Note is edited with a Tag that already exists in the address book, the + // Tag that the Note points to is the same object as the Tag that exists in the address book. + @Test + public void execute_editNoteWithTag_tagAlreadyExistsInTagMapping() throws Exception { + Model model = new ModelManager(); + String tagName = "Operations"; + model.addNote(new NoteBuilder().build()); + model.addTag(new Tag(tagName)); + + assertAll(() -> new EditNoteCommandParser(model).parse(" " + + "1" + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + List<Tag> listOfTagsFromNote = new ArrayList<>(model.getAddressBook().getNoteBook().get(0).getTags()); + Tag tagFromNote = listOfTagsFromNote.get(0); + Tag tagFromTagMapping = model.getTagMapping().get(tagName); + + assertSame(tagFromTagMapping, tagFromNote); + } + + @Test + public void execute_editNoteWithTag_deletesTagFromTagMapping() { + Model model = new ModelManager(); + String tagName = "Operations"; + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + NoteBuilder.DEFAULT_TITLE + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new EditNoteCommandParser(model).parse(" " + + "1" + " " + + CliSyntax.PREFIX_TAG + " ") + .execute(model)); + + assertFalse(model.getTagMapping().containsKey(tagName)); + } + + // Tag being added already exists in UniqueTagMapping because a Person has it + @Test + public void execute_editNoteWithTag_tagAlreadyExistsInTagMappingDueToPerson() throws Exception { + Model model = new ModelManager(); + model.addNote(new NoteBuilder().build()); + String tagName = "Operations"; + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + PersonBuilder.DEFAULT_NAME + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + assertAll(() -> new EditNoteCommandParser(model).parse(" " + + "1" + " " + + CliSyntax.PREFIX_TAG + tagName + " ") + .execute(model)); + + List<Tag> listOfTagsFromNote = new ArrayList<>(model.getAddressBook().getNoteBook().get(0).getTags()); + Tag tagFromNote = listOfTagsFromNote.get(0); + Tag tagFromTagMapping = model.getTagMapping().get(tagName); + + assertSame(tagFromTagMapping, tagFromNote); + } + + // Tag is not deleted from UniqueTagMapping when there still exists a Person with that Tag + @Test + public void execute_editNoteWithTag_doesNotDeletesTagFromTagMapping() { + Model model = new ModelManager(); + String tagName = "Operations"; + + assertAll(() -> new AddCommandParser(model).parse(" " + + CliSyntax.PREFIX_NAME + PersonBuilder.DEFAULT_NAME + " " + + CliSyntax.PREFIX_PHONE + PersonBuilder.DEFAULT_PHONE + " " + + CliSyntax.PREFIX_ADDRESS + PersonBuilder.DEFAULT_ADDRESS + " " + + CliSyntax.PREFIX_EMAIL + PersonBuilder.DEFAULT_EMAIL + " " + + CliSyntax.PREFIX_BIRTHDAY + PersonBuilder.DEFAULT_BIRTHDAY + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + NoteBuilder.DEFAULT_TITLE + " " + + CliSyntax.PREFIX_NOTES_CONTENT + NoteBuilder.DEFAULT_CONTENT + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + + assertAll(() -> new EditNoteCommandParser(model).parse(" " + + "1" + " " + + CliSyntax.PREFIX_TAG + " ") + .execute(model)); + + assertTrue(model.getTagMapping().containsKey(tagName)); + } + + @Test + public void equals() { + final EditNoteCommand standardCommand = new EditNoteCommand(INDEX_FIRST_NOTE, DESC_MEETING); + + // same values -> returns true + EditNoteDescriptor copyDescriptor = new EditNoteDescriptor(DESC_MEETING); + EditNoteCommand commandWithSameValues = new EditNoteCommand(INDEX_FIRST_NOTE, copyDescriptor); + 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 EditNoteCommand(INDEX_SECOND_NOTE, DESC_MEETING))); + + // different descriptor -> returns false + assertFalse(standardCommand.equals(new EditNoteCommand(INDEX_FIRST_NOTE, DESC_CLUB))); + } + +} diff --git a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java b/src/test/java/seedu/address/logic/commands/ExitCommandTest.java index 9533c473875..766019fbc1f 100644 --- a/src/test/java/seedu/address/logic/commands/ExitCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ExitCommandTest.java @@ -14,7 +14,8 @@ public class ExitCommandTest { @Test public void execute_exit_success() { - CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + CommandResult expectedCommandResult = new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, + CommandResult.UiState.Exit); assertCommandSuccess(new ExitCommand(), model, expectedCommandResult, expectedModel); } } diff --git a/src/test/java/seedu/address/logic/commands/FindNoteCommandTest.java b/src/test/java/seedu/address/logic/commands/FindNoteCommandTest.java new file mode 100644 index 00000000000..64c3cd7a85b --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FindNoteCommandTest.java @@ -0,0 +1,87 @@ +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.commons.core.Messages.MESSAGE_NOTES_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_NON_MATCHING_NOTE_TITLE; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalNotes.CHARITY; +import static seedu.address.testutil.TypicalNotes.DONATE; +import static seedu.address.testutil.TypicalNotes.ELECTION; +import static seedu.address.testutil.TypicalNotes.KEYWORD_MATCHING_EVENT; +import static seedu.address.testutil.TypicalNotes.getTypicalAddressBook; + +import java.util.Arrays; +import java.util.Collections; + +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.note.TitleContainsKeywordsPredicate; + +/** + * Contains integration tests (interaction with the Model) for {@code FindNoteCommand}. + */ +public class FindNoteCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void equals() { + TitleContainsKeywordsPredicate firstPredicate = + new TitleContainsKeywordsPredicate(Collections.singletonList("first")); + + TitleContainsKeywordsPredicate secondPredicate = + new TitleContainsKeywordsPredicate(Collections.singletonList("second")); + + FindNoteCommand findFirstNoteCommand = new FindNoteCommand(firstPredicate); + FindNoteCommand findSecondNoteCommand = new FindNoteCommand(secondPredicate); + + // same object -> returns true + assertTrue(findFirstNoteCommand.equals(findFirstNoteCommand)); + + // same values -> returns true + FindNoteCommand findFirstNoteCommandCopy = new FindNoteCommand(firstPredicate); + assertTrue(findFirstNoteCommand.equals(findFirstNoteCommandCopy)); + + // different types -> returns false + assertFalse(findFirstNoteCommand.equals(1)); + + // null -> returns false + assertFalse(findFirstNoteCommand.equals(null)); + + // different person -> returns false + assertFalse(findFirstNoteCommand.equals(findSecondNoteCommand)); + } + + @Test + public void execute_zeroKeywords_noNoteFound() { + String expectedMessage = String.format(MESSAGE_NOTES_LISTED_OVERVIEW, 0); + TitleContainsKeywordsPredicate predicate = preparePredicate(INVALID_NON_MATCHING_NOTE_TITLE); + FindNoteCommand command = new FindNoteCommand(predicate); + expectedModel.updateFilteredNoteList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredNoteList()); + } + + @Test + public void execute_multipleKeywords_multipleNoteFound() { + String expectedMessage = String.format(MESSAGE_NOTES_LISTED_OVERVIEW, 3); + TitleContainsKeywordsPredicate predicate = preparePredicate(KEYWORD_MATCHING_EVENT); + FindNoteCommand command = new FindNoteCommand(predicate); + expectedModel.updateFilteredNoteList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(CHARITY, DONATE, ELECTION), model.getFilteredNoteList()); + } + + + /** + * Parses {@code userInput} into a {@code TitleContainsKeywordsPredicate}. + */ + private TitleContainsKeywordsPredicate preparePredicate(String userInput) { + return new TitleContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/FindTagCommandTest.java b/src/test/java/seedu/address/logic/commands/FindTagCommandTest.java new file mode 100644 index 00000000000..9eaf6aa5152 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FindTagCommandTest.java @@ -0,0 +1,172 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertAll; +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.commons.core.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.DANIEL; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.parser.AddNoteCommandParser; +import seedu.address.logic.parser.CliSyntax; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.note.Content; +import seedu.address.model.note.Note; +import seedu.address.model.note.NoteTagsContainsKeywordsPredicate; +import seedu.address.model.note.Title; +import seedu.address.model.person.PersonTagsContainsKeywordsPredicate; +import seedu.address.model.tag.Tag; + +/** + * Contains integration tests (interaction with the Model) for {@code FindCommand}. + */ +public class FindTagCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void equals() { + PersonTagsContainsKeywordsPredicate firstPersonPredicate = + new PersonTagsContainsKeywordsPredicate(Collections.singletonList("first")); + PersonTagsContainsKeywordsPredicate secondPersonPredicate = + new PersonTagsContainsKeywordsPredicate(Collections.singletonList("second")); + NoteTagsContainsKeywordsPredicate firstNotePredicate = + new NoteTagsContainsKeywordsPredicate(Collections.singletonList("first")); + NoteTagsContainsKeywordsPredicate secondNotePredicate = + new NoteTagsContainsKeywordsPredicate(Collections.singletonList("second")); + + FindTagCommand findTagFirstCommand = new FindTagCommand(firstPersonPredicate, firstNotePredicate); + FindTagCommand findTagSecondCommand = new FindTagCommand(secondPersonPredicate, secondNotePredicate); + + // same object -> returns true + assertTrue(findTagFirstCommand.equals(findTagFirstCommand)); + + // same values -> returns true + FindTagCommand findTagFirstCommandCopy = new FindTagCommand(firstPersonPredicate, firstNotePredicate); + assertTrue(findTagFirstCommand.equals(findTagFirstCommandCopy)); + + // different types -> returns false + assertFalse(findTagFirstCommand.equals(1)); + + // null -> returns false + assertFalse(findTagFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(findTagFirstCommand.equals(findTagSecondCommand)); + } + + @Test + public void execute_zeroKeywords_noPersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + PersonTagsContainsKeywordsPredicate personPredicate = preparePersonPredicate(" "); + NoteTagsContainsKeywordsPredicate notePredicate = prepareNotePredicate(" "); + FindTagCommand command = new FindTagCommand(personPredicate, notePredicate); + expectedModel.updateFilteredPersonList(personPredicate); + expectedModel.updateFilteredNoteList(notePredicate); + 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); + PersonTagsContainsKeywordsPredicate personPredicate = preparePersonPredicate("friends owesMoney"); + NoteTagsContainsKeywordsPredicate notePredicate = prepareNotePredicate("friends owesMoney"); + FindTagCommand command = new FindTagCommand(personPredicate, notePredicate); + expectedModel.updateFilteredPersonList(personPredicate); + expectedModel.updateFilteredNoteList(notePredicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE, BENSON, DANIEL), model.getFilteredPersonList()); + } + + @Test + public void execute_zeroKeywords_noPersonOrNoteFound() { + String noteTitle = "Club meeting soon!"; + String noteContent = "Remind club members to attend meeting."; + String tagName = "friends"; + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + noteTitle + " " + + CliSyntax.PREFIX_NOTES_CONTENT + noteContent + " " + + CliSyntax.PREFIX_TAG + tagName) + .execute(model)); + + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + PersonTagsContainsKeywordsPredicate personPredicate = preparePersonPredicate(" "); + NoteTagsContainsKeywordsPredicate notePredicate = prepareNotePredicate(" "); + FindTagCommand command = new FindTagCommand(personPredicate, notePredicate); + expectedModel.updateFilteredPersonList(personPredicate); + expectedModel.updateFilteredNoteList(notePredicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredPersonList()); + assertEquals(Collections.emptyList(), model.getFilteredNoteList()); + } + + @Test + public void execute_multipleKeywords_multiplePersonsAndNotesFound() { + String noteTitleA = "Club meeting soon!"; + String noteContentA = "Remind club members to attend meeting."; + String tagNameA = "friends"; + + String noteTitleB = "T-Shirt payment due"; + String noteContentB = "Collect money"; + String tagNameB = "owesMoney"; + + Note noteA = new Note( + new Title(noteTitleA), + new Content(noteContentA), + new HashSet<>(Arrays.asList(new Tag(tagNameA)))); + Note noteB = new Note( + new Title(noteTitleB), + new Content(noteContentB), + new HashSet<>(Arrays.asList(new Tag(tagNameB)))); + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + noteTitleA + " " + + CliSyntax.PREFIX_NOTES_CONTENT + noteContentA + " " + + CliSyntax.PREFIX_TAG + tagNameA) + .execute(model)); + + assertAll(() -> new AddNoteCommandParser(model).parse(" " + + CliSyntax.PREFIX_NOTES_TITLE + noteTitleB + " " + + CliSyntax.PREFIX_NOTES_CONTENT + noteContentB + " " + + CliSyntax.PREFIX_TAG + tagNameB) + .execute(model)); + + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); + PersonTagsContainsKeywordsPredicate personPredicate = preparePersonPredicate("friends owesMoney"); + NoteTagsContainsKeywordsPredicate notePredicate = prepareNotePredicate("friends owesMoney"); + FindTagCommand command = new FindTagCommand(personPredicate, notePredicate); + expectedModel.updateFilteredPersonList(personPredicate); + expectedModel.updateFilteredNoteList(notePredicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE, BENSON, DANIEL), model.getFilteredPersonList()); + assertEquals(Arrays.asList(noteA, noteB), model.getFilteredNoteList()); + } + + /** + * Parses {@code userInput} into a {@code PersonTagsContainsKeywordsPredicate}. + */ + private PersonTagsContainsKeywordsPredicate preparePersonPredicate(String userInput) { + return new PersonTagsContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); + } + + /** + * Parses {@code userInput} into a {@code NoteTagsContainsKeywordsPredicate}. + */ + private NoteTagsContainsKeywordsPredicate prepareNotePredicate(String userInput) { + return new NoteTagsContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java b/src/test/java/seedu/address/logic/commands/HelpCommandTest.java index 4904fc4352e..4e51b499b1b 100644 --- a/src/test/java/seedu/address/logic/commands/HelpCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/HelpCommandTest.java @@ -14,7 +14,7 @@ public class HelpCommandTest { @Test public void execute_help_success() { - CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, true, false); + CommandResult expectedCommandResult = new CommandResult(SHOWING_HELP_MESSAGE, CommandResult.UiState.ShowHelp); assertCommandSuccess(new HelpCommand(), model, expectedCommandResult, expectedModel); } } diff --git a/src/test/java/seedu/address/logic/commands/ListNoteCommandTest.java b/src/test/java/seedu/address/logic/commands/ListNoteCommandTest.java new file mode 100644 index 00000000000..7eef98ee8f2 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ListNoteCommandTest.java @@ -0,0 +1,39 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showNoteAtIndex; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_NOTE; +import static seedu.address.testutil.TypicalNotes.getTypicalAddressBook; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; + +/** + * Contains integration tests (interaction with the Model) and unit tests for ListNoteCommand. + */ +public class ListNoteCommandTest { + + private Model model; + private Model expectedModel; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + } + + @Test + public void execute_listIsNotFiltered_showsSameList() { + assertCommandSuccess(new ListNoteCommand(), model, ListNoteCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_listIsFiltered_showsEverything() { + showNoteAtIndex(model, INDEX_FIRST_NOTE); + assertCommandSuccess(new ListNoteCommand(), model, ListNoteCommand.MESSAGE_SUCCESS, expectedModel); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java index 5cf487d7ebb..3a268dae378 100644 --- a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java @@ -3,9 +3,12 @@ import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_BOB; +import static seedu.address.logic.commands.CommandTestUtil.BIRTHDAY_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.BIRTHDAY_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.INVALID_ADDRESS_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_BIRTHDAY_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_PHONE_DESC; @@ -19,6 +22,7 @@ import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; 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_BIRTHDAY_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; @@ -32,7 +36,10 @@ import org.junit.jupiter.api.Test; import seedu.address.logic.commands.AddCommand; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; @@ -41,7 +48,8 @@ import seedu.address.testutil.PersonBuilder; public class AddCommandParserTest { - private AddCommandParser parser = new AddCommandParser(); + private Model model = new ModelManager(); + private AddCommandParser parser = new AddCommandParser(model); @Test public void parse_allFieldsPresent_success() { @@ -49,36 +57,41 @@ public void parse_allFieldsPresent_success() { // 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 + BIRTHDAY_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); // multiple names - last name accepted assertParseSuccess(parser, NAME_DESC_AMY + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); + + ADDRESS_DESC_BOB + BIRTHDAY_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); // multiple phones - last phone accepted assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_AMY + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); + + ADDRESS_DESC_BOB + BIRTHDAY_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); // multiple emails - last email accepted assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_AMY + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); + + ADDRESS_DESC_BOB + BIRTHDAY_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); // multiple addresses - last address accepted assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_AMY - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); + + ADDRESS_DESC_BOB + BIRTHDAY_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); + + // multiple birthdays - last birthday accepted + assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + BIRTHDAY_DESC_AMY + BIRTHDAY_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); // multiple tags - all accepted Person expectedPersonMultipleTags = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) .build(); assertParseSuccess(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, new AddCommand(expectedPersonMultipleTags)); + + BIRTHDAY_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, new AddCommand(expectedPersonMultipleTags)); } @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 + BIRTHDAY_DESC_AMY, new AddCommand(expectedPerson)); } @@ -87,23 +100,33 @@ 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 + BIRTHDAY_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 + BIRTHDAY_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 + BIRTHDAY_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 + BIRTHDAY_DESC_BOB, + expectedMessage); + + // missing birthday prefix + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + VALID_BIRTHDAY_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_BIRTHDAY_BOB, expectedMessage); } @@ -111,31 +134,35 @@ 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 - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Name.MESSAGE_CONSTRAINTS); + + BIRTHDAY_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 - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Phone.MESSAGE_CONSTRAINTS); + + BIRTHDAY_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 - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Email.MESSAGE_CONSTRAINTS); + + BIRTHDAY_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 - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Address.MESSAGE_CONSTRAINTS); + + BIRTHDAY_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Address.MESSAGE_CONSTRAINTS); + + // invalid birthday + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + INVALID_BIRTHDAY_DESC + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Birthday.MESSAGE_CONSTRAINTS); // invalid tag assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + INVALID_TAG_DESC + VALID_TAG_FRIEND, Tag.MESSAGE_CONSTRAINTS); + + BIRTHDAY_DESC_BOB + 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, - Name.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC + + BIRTHDAY_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 + BIRTHDAY_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 d9659205b57..a13cce9f69f 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -23,6 +23,8 @@ import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; import seedu.address.testutil.EditPersonDescriptorBuilder; @@ -32,24 +34,26 @@ public class AddressBookParserTest { private final AddressBookParser parser = new AddressBookParser(); + private final Model model = new ModelManager(); @Test public void parseCommand_add() throws Exception { Person person = new PersonBuilder().build(); - AddCommand command = (AddCommand) parser.parseCommand(PersonUtil.getAddCommand(person)); + AddCommand command = (AddCommand) parser.parseCommand(PersonUtil.getAddCommand(person), model); assertEquals(new AddCommand(person), command); } @Test public void parseCommand_clear() throws Exception { - assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD) instanceof ClearCommand); - assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD + " 3") instanceof ClearCommand); + assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD, model) instanceof ClearCommand); + assertTrue(parser.parseCommand(ClearCommand.COMMAND_WORD + " 3", model) + instanceof ClearCommand); } @Test public void parseCommand_delete() throws Exception { DeleteCommand command = (DeleteCommand) parser.parseCommand( - DeleteCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased()); + DeleteCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased(), model); assertEquals(new DeleteCommand(INDEX_FIRST_PERSON), command); } @@ -58,44 +62,49 @@ public void parseCommand_edit() throws Exception { Person person = new PersonBuilder().build(); EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(person).build(); EditCommand command = (EditCommand) parser.parseCommand(EditCommand.COMMAND_WORD + " " - + INDEX_FIRST_PERSON.getOneBased() + " " + PersonUtil.getEditPersonDescriptorDetails(descriptor)); + + INDEX_FIRST_PERSON.getOneBased() + " " + PersonUtil.getEditPersonDescriptorDetails(descriptor), + model); assertEquals(new EditCommand(INDEX_FIRST_PERSON, descriptor), command); } @Test public void parseCommand_exit() throws Exception { - assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD) instanceof ExitCommand); - assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); + assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD, model) instanceof ExitCommand); + assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD + " 3", model) instanceof ExitCommand); } @Test public void parseCommand_find() throws Exception { List<String> keywords = Arrays.asList("foo", "bar", "baz"); FindCommand command = (FindCommand) parser.parseCommand( - FindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); + FindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" ")), + model); assertEquals(new FindCommand(new NameContainsKeywordsPredicate(keywords)), command); } @Test public void parseCommand_help() throws Exception { - assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD) instanceof HelpCommand); - assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD + " 3") instanceof HelpCommand); + assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD, model) instanceof HelpCommand); + assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD + " 3", model) instanceof HelpCommand); } @Test public void parseCommand_list() throws Exception { - assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD) instanceof ListCommand); - assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); + assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD, model) instanceof ListCommand); + assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3", model) instanceof ListCommand); } @Test public void parseCommand_unrecognisedInput_throwsParseException() { assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), () - -> parser.parseCommand("")); + -> parser.parseCommand("", model)); } + @Test public void parseCommand_unknownCommand_throwsParseException() { - assertThrows(ParseException.class, MESSAGE_UNKNOWN_COMMAND, () -> parser.parseCommand("unknownCommand")); + assertThrows(ParseException.class, MESSAGE_UNKNOWN_COMMAND, + //CHECKSTYLE.OFF: SeparatorWrap + () -> parser.parseCommand("unknownCommand", model)); } } diff --git a/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java index 27eaec84450..9e4026eba7d 100644 --- a/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/DeleteCommandParserTest.java @@ -1,13 +1,12 @@ package seedu.address.logic.parser; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -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 org.junit.jupiter.api.Test; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.model.ModelManager; /** * As we are only doing white-box testing, our test cases do not cover path variations @@ -18,15 +17,10 @@ */ public class DeleteCommandParserTest { - private DeleteCommandParser parser = new DeleteCommandParser(); + private DeleteCommandParser parser = new DeleteCommandParser(new ModelManager()); @Test public void parse_validArgs_returnsDeleteCommand() { assertParseSuccess(parser, "1", new DeleteCommand(INDEX_FIRST_PERSON)); } - - @Test - public void parse_invalidArgs_throwsParseException() { - assertParseFailure(parser, "a", String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); - } } diff --git a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java index 2ff31522486..e20afe419c7 100644 --- a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java @@ -1,11 +1,15 @@ package seedu.address.logic.parser; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_NON_POSITIVE_INDEX; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_BOB; +import static seedu.address.logic.commands.CommandTestUtil.BIRTHDAY_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.BIRTHDAY_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.INVALID_ADDRESS_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_BIRTHDAY_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_PHONE_DESC; @@ -17,6 +21,8 @@ import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_AMY; import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_BIRTHDAY_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_BIRTHDAY_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_NAME_AMY; @@ -36,7 +42,9 @@ import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.model.ModelManager; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; @@ -50,12 +58,12 @@ public class EditCommandParserTest { private static final String MESSAGE_INVALID_FORMAT = String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE); - private EditCommandParser parser = new EditCommandParser(); + private EditCommandParser parser = new EditCommandParser(new ModelManager()); @Test public void parse_missingParts_failure() { - // no index specified - assertParseFailure(parser, VALID_NAME_AMY, MESSAGE_INVALID_FORMAT); + // deleted, ability added, previously: no index specified + // assertParseFailure(parser, VALID_NAME_AMY, MESSAGE_INVALID_FORMAT); // no field specified assertParseFailure(parser, "1", EditCommand.MESSAGE_NOT_EDITED); @@ -67,16 +75,13 @@ public void parse_missingParts_failure() { @Test public void parse_invalidPreamble_failure() { // negative index - assertParseFailure(parser, "-5" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "-5", MESSAGE_INVALID_NON_POSITIVE_INDEX); // zero index - assertParseFailure(parser, "0" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + assertParseFailure(parser, "0", MESSAGE_INVALID_NON_POSITIVE_INDEX); - // 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); + // invalid preamble + assertParseFailure(parser, "gfeui*(^", MESSAGE_INVALID_FORMAT); } @Test @@ -85,6 +90,7 @@ public void parse_invalidValue_failure() { assertParseFailure(parser, "1" + INVALID_PHONE_DESC, Phone.MESSAGE_CONSTRAINTS); // invalid phone assertParseFailure(parser, "1" + INVALID_EMAIL_DESC, Email.MESSAGE_CONSTRAINTS); // invalid email assertParseFailure(parser, "1" + INVALID_ADDRESS_DESC, Address.MESSAGE_CONSTRAINTS); // invalid address + assertParseFailure(parser, "1" + INVALID_BIRTHDAY_DESC, Birthday.MESSAGE_CONSTRAINTS); // invalid address assertParseFailure(parser, "1" + INVALID_TAG_DESC, Tag.MESSAGE_CONSTRAINTS); // invalid tag // invalid phone followed by valid email @@ -101,7 +107,8 @@ public void parse_invalidValue_failure() { assertParseFailure(parser, "1" + TAG_EMPTY + TAG_DESC_FRIEND + TAG_DESC_HUSBAND, Tag.MESSAGE_CONSTRAINTS); // multiple invalid values, but only the first invalid value is captured - assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_EMAIL_DESC + VALID_ADDRESS_AMY + VALID_PHONE_AMY, + assertParseFailure(parser, "1" + INVALID_NAME_DESC + INVALID_EMAIL_DESC + VALID_ADDRESS_AMY + + VALID_PHONE_AMY + VALID_ADDRESS_AMY, Name.MESSAGE_CONSTRAINTS); } @@ -109,10 +116,11 @@ public void parse_invalidValue_failure() { public void parse_allFieldsSpecified_success() { Index targetIndex = INDEX_SECOND_PERSON; String userInput = targetIndex.getOneBased() + PHONE_DESC_BOB + TAG_DESC_HUSBAND - + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + NAME_DESC_AMY + TAG_DESC_FRIEND; + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + NAME_DESC_AMY + BIRTHDAY_DESC_AMY + TAG_DESC_FRIEND; EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) + .withBirthday(VALID_BIRTHDAY_AMY) .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); @@ -158,6 +166,12 @@ public void parse_oneFieldSpecified_success() { expectedCommand = new EditCommand(targetIndex, descriptor); assertParseSuccess(parser, userInput, expectedCommand); + // birthday + userInput = targetIndex.getOneBased() + BIRTHDAY_DESC_AMY; + descriptor = new EditPersonDescriptorBuilder().withBirthday(VALID_BIRTHDAY_AMY).build(); + expectedCommand = new EditCommand(targetIndex, descriptor); + assertParseSuccess(parser, userInput, expectedCommand); + // tags userInput = targetIndex.getOneBased() + TAG_DESC_FRIEND; descriptor = new EditPersonDescriptorBuilder().withTags(VALID_TAG_FRIEND).build(); @@ -169,11 +183,13 @@ public void parse_oneFieldSpecified_success() { public void parse_multipleRepeatedFields_acceptsLast() { Index targetIndex = INDEX_FIRST_PERSON; String userInput = targetIndex.getOneBased() + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY - + TAG_DESC_FRIEND + PHONE_DESC_AMY + ADDRESS_DESC_AMY + EMAIL_DESC_AMY + TAG_DESC_FRIEND - + PHONE_DESC_BOB + ADDRESS_DESC_BOB + EMAIL_DESC_BOB + TAG_DESC_HUSBAND; + + TAG_DESC_FRIEND + PHONE_DESC_AMY + ADDRESS_DESC_AMY + BIRTHDAY_DESC_AMY + EMAIL_DESC_AMY + + TAG_DESC_FRIEND + PHONE_DESC_BOB + ADDRESS_DESC_BOB + BIRTHDAY_DESC_BOB + EMAIL_DESC_BOB + + TAG_DESC_HUSBAND; EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB) - .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) + .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withBirthday(VALID_BIRTHDAY_BOB) + .withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) .build(); EditCommand expectedCommand = new EditCommand(targetIndex, descriptor); @@ -191,9 +207,9 @@ public void parse_invalidValueFollowedByValidValue_success() { // other valid values specified userInput = targetIndex.getOneBased() + EMAIL_DESC_BOB + INVALID_PHONE_DESC + ADDRESS_DESC_BOB - + PHONE_DESC_BOB; + + PHONE_DESC_BOB + BIRTHDAY_DESC_BOB; descriptor = new EditPersonDescriptorBuilder().withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB) - .withAddress(VALID_ADDRESS_BOB).build(); + .withAddress(VALID_ADDRESS_BOB).withBirthday(VALID_BIRTHDAY_BOB).build(); expectedCommand = new EditCommand(targetIndex, descriptor); assertParseSuccess(parser, userInput, expectedCommand); } diff --git a/src/test/java/seedu/address/logic/parser/EditLoanCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditLoanCommandParserTest.java new file mode 100644 index 00000000000..65d5524773f --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/EditLoanCommandParserTest.java @@ -0,0 +1,108 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_NON_POSITIVE_INDEX; +import static seedu.address.logic.commands.CommandTestUtil.AMOUNT_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_AMOUNT_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_REASON_DESC; +import static seedu.address.logic.commands.CommandTestUtil.REASON_DESC; +import static seedu.address.logic.commands.CommandTestUtil.VALID_AMOUNT; +import static seedu.address.logic.commands.CommandTestUtil.VALID_REASON; +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 static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditLoanCommand; +import seedu.address.model.ModelManager; +import seedu.address.model.person.Loan; +import seedu.address.model.person.LoanHistory; +import seedu.address.model.person.Reason; + +public class EditLoanCommandParserTest { + + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditLoanCommand.MESSAGE_USAGE); + + private EditLoanCommandParser parser = new EditLoanCommandParser(new ModelManager()); + + @Test + public void parse_missingParts_failure() { + // deleted, ability added, previously: no index specified + // assertParseFailure(parser, VALID_NAME_AMY, MESSAGE_INVALID_FORMAT); + + // no field specified + assertParseFailure(parser, "1", Messages.AMOUNT_NOT_SPECIFIED); + assertParseFailure(parser, "1 amt/3", Messages.REASON_NOT_SPECIFIED); + + // no index and no field specified + assertParseFailure(parser, "", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + AMOUNT_DESC + REASON_DESC, + MESSAGE_INVALID_NON_POSITIVE_INDEX); + + // zero index + assertParseFailure(parser, "0" + AMOUNT_DESC + REASON_DESC, + MESSAGE_INVALID_NON_POSITIVE_INDEX); + + // invalid preamble + assertParseFailure(parser, "(&^(" + AMOUNT_DESC + REASON_DESC, + MESSAGE_INVALID_FORMAT); + + // invalid preamble + assertParseFailure(parser, "313-" + AMOUNT_DESC + REASON_DESC, + MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidValue_failure() { + // invalid amount, valid reason + assertParseFailure(parser, "1" + INVALID_AMOUNT_DESC + REASON_DESC, Loan.MESSAGE_CONSTRAINTS); + // valid amount, invalid reason + assertParseFailure(parser, "1" + AMOUNT_DESC + INVALID_REASON_DESC, Reason.MESSAGE_CONSTRAINTS); + + // loan is always captured first if invalid + assertParseFailure(parser, "1" + INVALID_AMOUNT_DESC + INVALID_REASON_DESC, + Loan.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1" + INVALID_REASON_DESC + INVALID_AMOUNT_DESC, + Loan.MESSAGE_CONSTRAINTS); + } + + @Test + public void parse_allFieldsSpecified_success() { + Index targetIndex = INDEX_SECOND_PERSON; + String userInput = targetIndex.getOneBased() + AMOUNT_DESC + REASON_DESC; + String userInputFlipped = targetIndex.getOneBased() + REASON_DESC + AMOUNT_DESC; + + EditLoanCommand.EditLoanDescriptor descriptor = + new EditLoanCommand.EditLoanDescriptor(new Loan(VALID_AMOUNT), + new LoanHistory(new Loan(VALID_AMOUNT), new Reason(VALID_REASON))); + + EditLoanCommand expectedCommand = new EditLoanCommand(targetIndex, descriptor); + + assertParseSuccess(parser, userInput, expectedCommand); + assertParseSuccess(parser, userInputFlipped, expectedCommand); + } + + @Test + public void parse_loanTooLarge_failure() { + String userInput = INDEX_FIRST_PERSON.getOneBased() + " amt/1000000000001" + REASON_DESC; + + assertParseFailure(parser, userInput, Loan.MESSAGE_CONSTRAINTS); + } + + @Test + public void parse_loanTooSmall_failure() { + String userInput = INDEX_FIRST_PERSON.getOneBased() + " amt/-1000000000000.1" + REASON_DESC; + + assertParseFailure(parser, userInput, Loan.MESSAGE_CONSTRAINTS); + } +} diff --git a/src/test/java/seedu/address/logic/parser/FindTagCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindTagCommandParserTest.java new file mode 100644 index 00000000000..a51c13434ba --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/FindTagCommandParserTest.java @@ -0,0 +1,37 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.FindTagCommand; +import seedu.address.model.note.NoteTagsContainsKeywordsPredicate; +import seedu.address.model.person.PersonTagsContainsKeywordsPredicate; + +public class FindTagCommandParserTest { + + private FindTagCommandParser parser = new FindTagCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindTagCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsFindTagCommand() { + // no leading and trailing whitespaces + FindTagCommand expectedFindTagCommand = + new FindTagCommand(new PersonTagsContainsKeywordsPredicate(Arrays.asList("Tech", "Finance")), + new NoteTagsContainsKeywordsPredicate(Arrays.asList("Tech", "Finance"))); + assertParseSuccess(parser, "Tech Finance", expectedFindTagCommand); + + // multiple whitespaces between keywords + assertParseSuccess(parser, " \n Tech \n \t Finance \t", expectedFindTagCommand); + } + +} diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java index 4256788b1a7..9523e6bdfba 100644 --- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java +++ b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java @@ -14,7 +14,10 @@ import org.junit.jupiter.api.Test; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; @@ -25,11 +28,13 @@ public class ParserUtilTest { 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_BIRTHDAY = "75435"; private static final String INVALID_TAG = "#friend"; private static final String VALID_NAME = "Rachel Walker"; private static final String VALID_PHONE = "123456"; private static final String VALID_ADDRESS = "123 Main Street #0505"; + private static final String VALID_BIRTHDAY = "01/01/2000"; 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"; @@ -148,48 +153,83 @@ public void parseEmail_validValueWithWhitespace_returnsTrimmedEmail() throws Exc assertEquals(expectedEmail, ParserUtil.parseEmail(emailWithWhitespace)); } + @Test + public void parseBirthday_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseBirthday((String) null)); + } + + @Test + public void parseBirthday_invalidValue_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseBirthday(INVALID_BIRTHDAY)); + } + + @Test + public void parseBirthday_validValueWithoutWhitespace_returnsBirthday() throws Exception { + Birthday expectedBirthday = new Birthday(VALID_BIRTHDAY); + assertEquals(expectedBirthday, ParserUtil.parseBirthday(VALID_BIRTHDAY)); + } + + @Test + public void parseBirthday_validValueWithWhitespace_returnsTrimmedBirthday() throws Exception { + String birthdayWithWhitespace = WHITESPACE + VALID_BIRTHDAY + WHITESPACE; + Birthday expectedBirthday = new Birthday(VALID_BIRTHDAY); + assertEquals(expectedBirthday, ParserUtil.parseBirthday(birthdayWithWhitespace)); + } + @Test public void parseTag_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parseTag(null)); + assertThrows(NullPointerException.class, () -> ParserUtil.parseTag(null, new ModelManager())); } @Test public void parseTag_invalidValue_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parseTag(INVALID_TAG)); + assertThrows(ParseException.class, () -> ParserUtil.parseTag(INVALID_TAG, new ModelManager())); } @Test public void parseTag_validValueWithoutWhitespace_returnsTag() throws Exception { Tag expectedTag = new Tag(VALID_TAG_1); - assertEquals(expectedTag, ParserUtil.parseTag(VALID_TAG_1)); + Model model = new ModelManager(); + model.addTag(expectedTag); + + assertEquals(expectedTag, ParserUtil.parseTag(VALID_TAG_1, model)); } @Test public void parseTag_validValueWithWhitespace_returnsTrimmedTag() throws Exception { String tagWithWhitespace = WHITESPACE + VALID_TAG_1 + WHITESPACE; Tag expectedTag = new Tag(VALID_TAG_1); - assertEquals(expectedTag, ParserUtil.parseTag(tagWithWhitespace)); + Model model = new ModelManager(); + model.addTag(expectedTag); + + assertEquals(expectedTag, ParserUtil.parseTag(tagWithWhitespace, model)); } @Test public void parseTags_null_throwsNullPointerException() { - assertThrows(NullPointerException.class, () -> ParserUtil.parseTags(null)); + assertThrows(NullPointerException.class, () -> ParserUtil.parseTags(null, new ModelManager())); } @Test public void parseTags_collectionWithInvalidTags_throwsParseException() { - assertThrows(ParseException.class, () -> ParserUtil.parseTags(Arrays.asList(VALID_TAG_1, INVALID_TAG))); + assertThrows(ParseException.class, + // CHECKSTYLE.OFF: SeparatorWrap + () -> ParserUtil.parseTags(Arrays.asList(VALID_TAG_1, INVALID_TAG), new ModelManager())); } @Test public void parseTags_emptyCollection_returnsEmptySet() throws Exception { - assertTrue(ParserUtil.parseTags(Collections.emptyList()).isEmpty()); + assertTrue(ParserUtil.parseTags(Collections.emptyList(), new ModelManager()).isEmpty()); } @Test public void parseTags_collectionWithValidTags_returnsTagSet() throws Exception { - Set<Tag> actualTagSet = ParserUtil.parseTags(Arrays.asList(VALID_TAG_1, VALID_TAG_2)); - Set<Tag> expectedTagSet = new HashSet<Tag>(Arrays.asList(new Tag(VALID_TAG_1), new Tag(VALID_TAG_2))); + Model model = new ModelManager(); + model.addTag(new Tag(VALID_TAG_1)); + model.addTag(new Tag(VALID_TAG_2)); + + Set<Tag> actualTagSet = ParserUtil.parseTags(Arrays.asList(VALID_TAG_1, VALID_TAG_2), model); + Set<Tag> expectedTagSet = new HashSet<>(Arrays.asList(new Tag(VALID_TAG_1), new Tag(VALID_TAG_2))); assertEquals(expectedTagSet, actualTagSet); } diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java index 87782528ecd..c43b9ebf990 100644 --- a/src/test/java/seedu/address/model/AddressBookTest.java +++ b/src/test/java/seedu/address/model/AddressBookTest.java @@ -18,8 +18,11 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import seedu.address.model.note.Note; import seedu.address.model.person.Person; import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.tag.Tag; import seedu.address.testutil.PersonBuilder; public class AddressBookTest { @@ -88,6 +91,8 @@ public void getPersonList_modifyList_throwsUnsupportedOperationException() { */ private static class AddressBookStub implements ReadOnlyAddressBook { private final ObservableList<Person> persons = FXCollections.observableArrayList(); + private final ObservableList<Note> notes = FXCollections.observableArrayList(); + private final ObservableMap<String, Tag> tags = FXCollections.observableHashMap(); AddressBookStub(Collection<Person> persons) { this.persons.setAll(persons); @@ -97,6 +102,16 @@ private static class AddressBookStub implements ReadOnlyAddressBook { public ObservableList<Person> getPersonList() { return persons; } + + @Override + public ObservableList<Note> getNoteBook() { + return notes; + } + + @Override + public ObservableMap<String, Tag> getTagMapping() { + return tags; + } } } diff --git a/src/test/java/seedu/address/model/note/NoteTagsContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/note/NoteTagsContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..d8c09948675 --- /dev/null +++ b/src/test/java/seedu/address/model/note/NoteTagsContainsKeywordsPredicateTest.java @@ -0,0 +1,82 @@ +package seedu.address.model.note; + +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.NoteBuilder; + +public class NoteTagsContainsKeywordsPredicateTest { + @Test + public void equals() { + List<String> firstPredicateKeywordList = Collections.singletonList("first"); + List<String> secondPredicateKeywordList = Arrays.asList("first", "second"); + + NoteTagsContainsKeywordsPredicate firstPredicate = + new NoteTagsContainsKeywordsPredicate(firstPredicateKeywordList); + NoteTagsContainsKeywordsPredicate secondPredicate = + new NoteTagsContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + NoteTagsContainsKeywordsPredicate firstPredicateCopy = + new NoteTagsContainsKeywordsPredicate(firstPredicateKeywordList); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different person -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_tagsContainsKeywords_returnsTrue() { + // One keyword + NoteTagsContainsKeywordsPredicate predicate = + new NoteTagsContainsKeywordsPredicate(Collections.singletonList("Tech")); + assertTrue(predicate.test(new NoteBuilder().withTags("Tech").build())); + + // Multiple keywords + predicate = new NoteTagsContainsKeywordsPredicate(Arrays.asList("Tech", "Finance")); + assertTrue(predicate.test(new NoteBuilder().withTags("Tech").build())); + + // Only one matching keyword + predicate = new NoteTagsContainsKeywordsPredicate(Arrays.asList("Finance", "Operations")); + assertTrue(predicate.test(new NoteBuilder().withTags("Finance").build())); + + // Mixed-case keywords + predicate = new NoteTagsContainsKeywordsPredicate(Arrays.asList("TeCh", "fINANce")); + assertTrue(predicate.test(new NoteBuilder().withTags("Finance").build())); + + // Multiple tags + predicate = new NoteTagsContainsKeywordsPredicate(Arrays.asList("Operations", "Tech")); + assertTrue(predicate.test(new NoteBuilder().withTags("Finance", "Operations", "Tech").build())); + } + + @Test + public void test_tagsDoesNotContainKeywords_returnsFalse() { + // Zero keywords + NoteTagsContainsKeywordsPredicate predicate = new NoteTagsContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new NoteBuilder().withTags("Tech").build())); + + // Non-matching keyword + predicate = new NoteTagsContainsKeywordsPredicate(Arrays.asList("Operations")); + assertFalse(predicate.test(new NoteBuilder().withTags("Tech").build())); + + // Keywords match title and content, but do not match any tags + predicate = new NoteTagsContainsKeywordsPredicate(Arrays.asList("Event", "Party")); + assertFalse(predicate.test(new NoteBuilder().withTitle("Event").withContent("Party") + .withTags("People").build())); + } +} diff --git a/src/test/java/seedu/address/model/person/AddressTest.java b/src/test/java/seedu/address/model/person/AddressTest.java index dcd3be87b3a..c658e112470 100644 --- a/src/test/java/seedu/address/model/person/AddressTest.java +++ b/src/test/java/seedu/address/model/person/AddressTest.java @@ -1,6 +1,8 @@ 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.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.testutil.Assert.assertThrows; @@ -33,4 +35,12 @@ public void isValidAddress() { assertTrue(Address.isValidAddress("-")); // one character assertTrue(Address.isValidAddress("Leng Inc; 1234 Market St; San Francisco CA 2349879; USA")); // long address } + + @Test + public void deepCopy_copyNotSameButEqual() { + Address address = new Address("Leng Inc; 1234 Market St; San Francisco CA 2349879; USA"); + Address deepCopy = address.deepCopy(); + assertNotSame(address, deepCopy); + assertEquals(address, deepCopy); + } } diff --git a/src/test/java/seedu/address/model/person/BirthdayTest.java b/src/test/java/seedu/address/model/person/BirthdayTest.java new file mode 100644 index 00000000000..c295a32d3bf --- /dev/null +++ b/src/test/java/seedu/address/model/person/BirthdayTest.java @@ -0,0 +1,59 @@ +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.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class BirthdayTest { + + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new Birthday(null)); + } + + @Test + public void constructor_invalidBirthday_throwsIllegalArgumentException() { + String invalidBirthday = ""; + assertThrows(IllegalArgumentException.class, () -> new Address(invalidBirthday)); + } + + @Test + public void isValidBirthday() { + // null birthday + assertThrows(NullPointerException.class, () -> Birthday.isValidBirthday(null)); + + // invalid birthdays + assertFalse(Birthday.isValidBirthday("")); // empty string + assertFalse(Birthday.isValidBirthday(" ")); // spaces only + assertFalse(Birthday.isValidBirthday("00/01/2000")); // Boundary case for start of month + assertFalse(Birthday.isValidBirthday("32/05/2000")); // Boundary case at end of month + assertFalse(Birthday.isValidBirthday("01/00/2000")); // Boundary case for start of year + assertFalse(Birthday.isValidBirthday("01/13/2000")); // Boundary case for end of year + assertFalse(Birthday.isValidBirthday("01/01/19999")); // invalid year only + assertFalse(Birthday.isValidBirthday("1/01/2000")); // day field not long enough + assertFalse(Birthday.isValidBirthday("01/6/2000")); // day field not long enough + assertFalse(Birthday.isValidBirthday("29/02/2001")); // 29th February on a non-leap year + assertFalse(Birthday.isValidBirthday("31/06/2000")); // day that does not exist + + + // valid birthdays + assertTrue(Birthday.isValidBirthday("15/06/2000")); // Normal date + assertTrue(Birthday.isValidBirthday("01/01/2000")); // Boundary case for start of month + assertTrue(Birthday.isValidBirthday("30/06/2000")); // Boundary case for end of month + assertTrue(Birthday.isValidBirthday("29/02/2000")); // 29th February on a leap year + + + } + + @Test + public void deepCopy_copyNotSameButEqual() { + Birthday birthday = new Birthday("01/01/2000"); + Birthday deepCopy = birthday.deepCopy(); + assertNotSame(birthday, deepCopy); + assertEquals(birthday, deepCopy); + } +} diff --git a/src/test/java/seedu/address/model/person/EmailTest.java b/src/test/java/seedu/address/model/person/EmailTest.java index bbcc6c8c98e..bacdf54679c 100644 --- a/src/test/java/seedu/address/model/person/EmailTest.java +++ b/src/test/java/seedu/address/model/person/EmailTest.java @@ -1,6 +1,8 @@ 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.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.testutil.Assert.assertThrows; @@ -65,4 +67,12 @@ public void isValidEmail() { assertTrue(Email.isValidEmail("if.you.dream.it_you.can.do.it@example.com")); // long local part assertTrue(Email.isValidEmail("e1234567@u.nus.edu")); // more than one period in domain } + + @Test + public void deepCopy_copyNotSameButEqual() { + Email email = new Email("e1234567@u.nus.edu"); + Email deepCopy = email.deepCopy(); + assertNotSame(email, deepCopy); + assertEquals(email, deepCopy); + } } diff --git a/src/test/java/seedu/address/model/person/LoanTest.java b/src/test/java/seedu/address/model/person/LoanTest.java new file mode 100644 index 00000000000..294f83ac186 --- /dev/null +++ b/src/test/java/seedu/address/model/person/LoanTest.java @@ -0,0 +1,180 @@ +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.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.exceptions.LoanOutOfBoundsException; + +public class LoanTest { + + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new Loan(null)); + } + + @Test + public void constructor_invalidLoan_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> + new Loan("")); // nothing given + assertThrows(IllegalArgumentException.class, () -> + new Loan("99.999")); // 3dp + assertThrows(IllegalArgumentException.class, () -> + new Loan("9999999999999")); // too large + assertThrows(IllegalArgumentException.class, () -> + new Loan("+$1000000000000.01")); // too large + assertThrows(IllegalArgumentException.class, () -> + new Loan("-1000000000000.01")); // too small + assertThrows(IllegalArgumentException.class, () -> + new Loan("$-99.8")); // dollar sign before minus + assertThrows(IllegalArgumentException.class, () -> + new Loan("-+99.8")); // both signs at the same time + } + + @Test + public void isValidLoan() { + // null loan + assertThrows(NullPointerException.class, () -> Loan.isValidLoan(null)); + + // invalid loan strings + assertFalse(Loan.isValidLoan("")); // empty string + assertFalse(Loan.isValidLoan(" ")); // spaces only + assertFalse(Loan.isValidLoan("^&92")); // unacceptable symbols + assertFalse(Loan.isValidLoan("loan")); // non-numeric + assertFalse(Loan.isValidLoan("9p0")); // alphabets within digits + assertFalse(Loan.isValidLoan("2 4")); // spaces within digits + assertFalse(Loan.isValidLoan("11.88234")); // more than 2 decimal places + assertFalse(Loan.isValidLoan("2 4")); // spaces within digits + assertFalse(Loan.isValidLoan("2$4")); // dollar sign in the middle + assertFalse(Loan.isValidLoan("1000000000000.01")); // one cent more than max + assertFalse(Loan.isValidLoan("-1000000000000.01")); // one cent less than min + assertFalse(Loan.isValidLoan("-1111111111111")); // number too small + assertFalse(Loan.isValidLoan("1732744702493240347")); // number too large + assertFalse(Loan.isValidLoan("-.3")); // missing ones place + + // valid phone numbers + assertTrue(Loan.isValidLoan("-12")); // negative numbers + assertTrue(Loan.isValidLoan("-10.57")); // negative numbers with decimal point + assertTrue(Loan.isValidLoan("-10.3")); // negative numbers with one decimal place + assertTrue(Loan.isValidLoan("12")); // positive numbers + assertTrue(Loan.isValidLoan("11.88")); // positive numbers with decimal point + assertTrue(Loan.isValidLoan("19946024893")); // large positive number + assertTrue(Loan.isValidLoan("-19946024893")); // large negative number + assertTrue(Loan.isValidLoan("-$190")); // dollar sign gets parsed out + assertTrue(Loan.isValidLoan("$53")); // dollar sign gets parsed out + assertTrue(Loan.isValidLoan("+$53")); // dollar sign with plus sign parsed out + assertTrue(Loan.isValidLoan("+53")); // plus sign parsed out + assertTrue(Loan.isValidLoan("1000000000000")); // maximum value + assertTrue(Loan.isValidLoan("1000000000000")); // minimum value + assertTrue(Loan.isValidLoan(1_000_000_000_000.00)); // max value + assertTrue(Loan.isValidLoan(-1_000_000_000_000.00)); // max value + assertTrue(Loan.isValidLoan(999_999_999_999.0)); // one dollar below max value + assertTrue(Loan.isValidLoan(999_999_999_999.99)); // one cent below max value + assertTrue(Loan.isValidLoan(-999_999_999_999.0)); // one dollar above min value + assertTrue(Loan.isValidLoan(-999_999_999_999.99)); // one cent above min value + assertTrue(Loan.isValidLoan("-$999999999999.99")); // one cent above min value + assertTrue(Loan.isValidLoan("$999999999999.99")); // one cent above min value + } + + @Test + public void loanAmountsAreTheSame() { + assertEquals(new Loan("33.55").getAmount(), 33.55); + assertEquals(new Loan("1000000000000").getAmount(), 1_000_000_000_000.0); + assertEquals(new Loan("999999999999").getAmount(), 999_999_999_999.0); + assertEquals(new Loan("999999999999.9").getAmount(), 999_999_999_999.9); + assertEquals(new Loan("999999999999.99").getAmount(), 999_999_999_999.99); + assertEquals(new Loan("0.55").getAmount(), 0.55); + assertEquals(new Loan("-0.55").getAmount(), -0.55); + assertEquals(new Loan("-999999999999.99").getAmount(), -999_999_999_999.99); + } + + @Test + public void loanSubtractionCorrect() { + try { + assertEquals(new Loan("33.55") + .subtractBy(new Loan("2.55")).getAmount(), 31); + } catch (LoanOutOfBoundsException e) { + fail(); + } + try { + assertEquals(new Loan("33.01") + .subtractBy(new Loan("0.59")).getAmount(), 32.42); + } catch (LoanOutOfBoundsException e) { + fail(); + } + + try { + assertEquals(new Loan("1000000000000") + .subtractBy(new Loan("0.01")).getAmount(), 999_999_999_999.99); + } catch (LoanOutOfBoundsException e) { + fail(); + } + + try { + assertEquals(new Loan("0") + .subtractBy(new Loan("0.01")).getAmount(), -0.01); + } catch (LoanOutOfBoundsException e) { + fail(); + } + } + + @Test + public void loanAdditionCorrect() { + try { + assertEquals(new Loan("33.55") + .addBy(new Loan("2.55")).getAmount(), 36.1); + } catch (LoanOutOfBoundsException e) { + fail(); + } + + try { + assertEquals(new Loan("33.21") + .addBy(new Loan("0.59")).getAmount(), 33.8); + } catch (LoanOutOfBoundsException e) { + fail(); + } + + try { + assertEquals(new Loan("999999999999.99") + .addBy(new Loan("0.01")).getAmount(), 1_000_000_000_000.0); + } catch (LoanOutOfBoundsException e) { + fail(); + } + + try { + assertEquals(new Loan("0") + .addBy(new Loan("0.01")).getAmount(), 0.01); + } catch (LoanOutOfBoundsException e) { + fail(); + } + } + + @Test + public void loanAddition_numberTooLarge_outOfBounds() { + assertThrows(LoanOutOfBoundsException.class, () -> + new Loan("1000000000000").addBy(new Loan("0.01"))); + assertThrows(LoanOutOfBoundsException.class, () -> + new Loan("999999999103").addBy(new Loan("1002"))); + } + + @Test + public void loanSubtraction_numberTooSmall_outOfBounds() { + assertThrows(LoanOutOfBoundsException.class, () -> + new Loan("-1000000000000").subtractBy(new Loan("0.01"))); + assertThrows(LoanOutOfBoundsException.class, () -> + new Loan("-999999999103").subtractBy(new Loan("1002"))); + } + + @Test + public void deepCopy_copyNotSameButEqual() { + Loan loan = new Loan("309"); + Loan deepCopy = loan.deepCopy(); + assertNotSame(loan, deepCopy); + assertEquals(loan, deepCopy); + } +} diff --git a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java index f136664e017..265de5a436f 100644 --- a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java +++ b/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java @@ -55,20 +55,25 @@ public void test_nameContainsKeywords_returnsTrue() { // Mixed-case keywords predicate = new NameContainsKeywordsPredicate(Arrays.asList("aLIce", "bOB")); assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); + + // Matching phone number + predicate = new NameContainsKeywordsPredicate(Collections.singletonList("123")); + assertTrue(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@email.com").withAddress("Main Street").build())); } @Test public void test_nameDoesNotContainKeywords_returnsFalse() { // Zero keywords NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Collections.emptyList()); - assertFalse(predicate.test(new PersonBuilder().withName("Alice").build())); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("123456789").build())); - // Non-matching keyword - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Carol")); - assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").build())); + // Non-matching keywords + predicate = new NameContainsKeywordsPredicate(Arrays.asList("Carol", "123")); + assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").withPhone("456789").build())); - // Keywords match phone, email and address, but does not match name - predicate = new NameContainsKeywordsPredicate(Arrays.asList("12345", "alice@email.com", "Main", "Street")); + // Keywords match email and address, but does not match name or phone + predicate = new NameContainsKeywordsPredicate(Arrays.asList("alice@email.com", "Main", "Street")); assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") .withEmail("alice@email.com").withAddress("Main Street").build())); } diff --git a/src/test/java/seedu/address/model/person/NameTest.java b/src/test/java/seedu/address/model/person/NameTest.java index c9801392874..a1db295a9e4 100644 --- a/src/test/java/seedu/address/model/person/NameTest.java +++ b/src/test/java/seedu/address/model/person/NameTest.java @@ -1,6 +1,8 @@ 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.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.testutil.Assert.assertThrows; @@ -26,15 +28,37 @@ public void isValidName() { // invalid 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 + assertFalse(Name.isValidName("12345")); // numbers only + assertFalse(Name.isValidName("2 Captain Jack Sparrow")); // standalone number + assertFalse(Name.isValidName("Captain Jack 2 Sparrow")); // standalone number + assertFalse(Name.isValidName("Captain Jack Sparrow 2")); // standalone number + assertFalse(Name.isValidName("313%$#")); // inclusion of invalid characters + assertFalse(Name.isValidName("314a!")); // inclusion of invalid characters + assertFalse(Name.isValidName("313-")); // inclusion of invalid characters + assertFalse(Name.isValidName("alpha 313-")); // first word valid, second invalid // valid name assertTrue(Name.isValidName("peter jack")); // alphabets only - assertTrue(Name.isValidName("12345")); // numbers only assertTrue(Name.isValidName("peter the 2nd")); // alphanumeric characters assertTrue(Name.isValidName("Capital Tan")); // with capital letters - assertTrue(Name.isValidName("David Roger Jackson Ray Jr 2nd")); // long names + assertTrue(Name.isValidName("Capital Tan no2")); // with capital letters and valid alphanumeric sequence + assertTrue(Name.isValidName("a2b")); + assertTrue(Name.isValidName("a987b")); + assertTrue(Name.isValidName("a98b76c543d2")); + assertTrue(Name.isValidName("a2")); + assertTrue(Name.isValidName("ab2")); + assertTrue(Name.isValidName("2a")); + assertTrue(Name.isValidName("2abc")); + assertTrue(Name.isValidName("a2b a234b")); + } + + @Test + public void deepCopy_copyNotSameButEqual() { + Name name = new Name("Capital Tan"); + Name deepCopy = name.deepCopy(); + assertNotSame(name, deepCopy); + assertEquals(name, deepCopy); } } diff --git a/src/test/java/seedu/address/model/person/PersonTagsContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/PersonTagsContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..9756d1c785f --- /dev/null +++ b/src/test/java/seedu/address/model/person/PersonTagsContainsKeywordsPredicateTest.java @@ -0,0 +1,84 @@ +package seedu.address.model.person; + +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 PersonTagsContainsKeywordsPredicateTest { + @Test + public void equals() { + List<String> firstPredicateKeywordList = Collections.singletonList("first"); + List<String> secondPredicateKeywordList = Arrays.asList("first", "second"); + + PersonTagsContainsKeywordsPredicate firstPredicate = + new PersonTagsContainsKeywordsPredicate(firstPredicateKeywordList); + PersonTagsContainsKeywordsPredicate secondPredicate = + new PersonTagsContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + PersonTagsContainsKeywordsPredicate firstPredicateCopy = + new PersonTagsContainsKeywordsPredicate(firstPredicateKeywordList); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different person -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_tagsContainsKeywords_returnsTrue() { + // One keyword + PersonTagsContainsKeywordsPredicate predicate = + new PersonTagsContainsKeywordsPredicate(Collections.singletonList("Tech")); + assertTrue(predicate.test(new PersonBuilder().withTags("Tech").build())); + + // Multiple keywords + predicate = new PersonTagsContainsKeywordsPredicate(Arrays.asList("Tech", "Finance")); + assertTrue(predicate.test(new PersonBuilder().withTags("Tech").build())); + + // Only one matching keyword + predicate = new PersonTagsContainsKeywordsPredicate(Arrays.asList("Finance", "Operations")); + assertTrue(predicate.test(new PersonBuilder().withTags("Finance").build())); + + // Mixed-case keywords + predicate = new PersonTagsContainsKeywordsPredicate(Arrays.asList("TeCh", "fINANce")); + assertTrue(predicate.test(new PersonBuilder().withTags("Finance").build())); + + // Multiple tags + predicate = new PersonTagsContainsKeywordsPredicate(Arrays.asList("Operations", "Tech")); + assertTrue(predicate.test(new PersonBuilder().withTags("Finance", "Operations", "Tech").build())); + } + + @Test + public void test_tagsDoesNotContainKeywords_returnsFalse() { + // Zero keywords + PersonTagsContainsKeywordsPredicate predicate = + new PersonTagsContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new PersonBuilder().withTags("Tech").build())); + + // Non-matching keyword + predicate = new PersonTagsContainsKeywordsPredicate(Arrays.asList("Operations")); + assertFalse(predicate.test(new PersonBuilder().withTags("Tech").build())); + + // Keywords match phone, email and address, but do not match any tags + predicate = + new PersonTagsContainsKeywordsPredicate(Arrays.asList("12345", "alice@email.com", "Main", "Street")); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@email.com").withAddress("Main Street").withTags("Tech").build())); + } +} diff --git a/src/test/java/seedu/address/model/person/PersonTest.java b/src/test/java/seedu/address/model/person/PersonTest.java index b29c097cfd4..02bc5bbf2c5 100644 --- a/src/test/java/seedu/address/model/person/PersonTest.java +++ b/src/test/java/seedu/address/model/person/PersonTest.java @@ -1,6 +1,9 @@ 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.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; 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_EMAIL_BOB; @@ -11,8 +14,14 @@ import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.BOB; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + import org.junit.jupiter.api.Test; +import seedu.address.model.tag.Tag; import seedu.address.testutil.PersonBuilder; public class PersonTest { @@ -88,4 +97,108 @@ public void equals() { editedAlice = new PersonBuilder(ALICE).withTags(VALID_TAG_HUSBAND).build(); assertFalse(ALICE.equals(editedAlice)); } + + @Test + public void deepCopy_notSameButEqual() { + String tagName = "House"; + Person personA = new PersonBuilder().withName("PersonA").withTags(tagName).build(); + Person deepCopy = personA.deepCopy(); + + assertNotSame(personA, deepCopy); + assertEquals(personA, deepCopy); + } + + @Test + public void deepCopy_tagsCopiedNotSame() { + String tagName = "House"; + Tag tag = new Tag(tagName); + Person personA = new Person( + new Name("PersonA"), + new Phone(PersonBuilder.DEFAULT_PHONE), + new Email(PersonBuilder.DEFAULT_EMAIL), + new Address(PersonBuilder.DEFAULT_ADDRESS), + new Birthday(PersonBuilder.DEFAULT_BIRTHDAY), + Set.of(tag), + new Loan(PersonBuilder.DEFAULT_LOAN), + new ArrayList<LoanHistory>()); + + tag.addPerson(personA); + + Person deepCopy = personA.deepCopy(); + + assertEquals(1, personA.getTags().size()); + assertEquals(1, deepCopy.getTags().size()); + assertNotSame(personA.getTags().toArray()[0], deepCopy.getTags().toArray()[0]); + assertEquals(personA.getTags().toArray()[0], deepCopy.getTags().toArray()[0]); + } + + @Test + @SuppressWarnings("unchecked") + public void deepCopy_tagsCopiedPointToOtherSamePerson() { + String tagName = "House"; + Tag tag = new Tag(tagName); + Set<Tag> tagSet = Set.of(tag); + Person personA = new Person( + new Name("PersonA"), + new Phone(PersonBuilder.DEFAULT_PHONE), + new Email(PersonBuilder.DEFAULT_EMAIL), + new Address(PersonBuilder.DEFAULT_ADDRESS), + new Birthday(PersonBuilder.DEFAULT_BIRTHDAY), + tagSet, + new Loan(PersonBuilder.DEFAULT_LOAN), + new ArrayList<LoanHistory>()); + + Person personB = new Person( + new Name("PersonB"), + new Phone(PersonBuilder.DEFAULT_PHONE), + new Email(PersonBuilder.DEFAULT_EMAIL), + new Address(PersonBuilder.DEFAULT_ADDRESS), + new Birthday(PersonBuilder.DEFAULT_BIRTHDAY), + tagSet, + new Loan(PersonBuilder.DEFAULT_LOAN), + new ArrayList<LoanHistory>()); + + tag.addPerson(personA); + tag.addPerson(personB); + + Person deepCopy = personA.deepCopy(); + Tag deepCopyTag = deepCopy.getTags().toArray(Tag[]::new)[0]; + + assertEquals(1, personA.getTags().size()); + assertEquals(1, deepCopy.getTags().size()); + + // By equality, personA and personB exist + assertTrue(deepCopyTag.getDeepCopiedPersonList().contains(personA)); + assertTrue(deepCopyTag.getDeepCopiedPersonList().contains(personB)); + + // for testing purposes, use reflection to set accessibility of a normally hidden field to true + List<Person> personList; + try { + Field personListField = deepCopyTag.getClass().getDeclaredField("personList"); + personListField.setAccessible(true); + personList = (List<Person>) personListField.get(deepCopyTag); + } catch (NoSuchFieldException | IllegalAccessException e) { + personList = new ArrayList<>(); + } + + // By reference, personB exists, but personA does not exist and is + // instead replaced by deepCopy + assertSame(personB, personList + .stream() + .filter(p -> p.equals(personB)) + .findFirst() + .orElseGet(() -> new PersonBuilder().build())); + + assertNotSame(personA, personList + .stream() + .filter(p -> p.equals(personA)) + .findFirst() + .orElseGet(() -> new PersonBuilder().build())); + + assertSame(deepCopy, personList + .stream() + .filter(p -> p.equals(personA)) + .findFirst() + .orElseGet(() -> new PersonBuilder().build())); + } } diff --git a/src/test/java/seedu/address/model/person/PhoneTest.java b/src/test/java/seedu/address/model/person/PhoneTest.java index 8dd52766a5f..54270a4ba7d 100644 --- a/src/test/java/seedu/address/model/person/PhoneTest.java +++ b/src/test/java/seedu/address/model/person/PhoneTest.java @@ -1,6 +1,8 @@ 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.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.testutil.Assert.assertThrows; @@ -37,4 +39,12 @@ public void isValidPhone() { assertTrue(Phone.isValidPhone("93121534")); assertTrue(Phone.isValidPhone("124293842033123")); // long phone numbers } + + @Test + public void deepCopy_copyNotSameButEqual() { + Phone phone = new Phone("93121534"); + Phone deepCopy = phone.deepCopy(); + assertNotSame(phone, deepCopy); + assertEquals(phone, deepCopy); + } } diff --git a/src/test/java/seedu/address/model/person/UniquePersonListTest.java b/src/test/java/seedu/address/model/person/UniquePersonListTest.java index 1cc5fe9e0fe..079a7b222cc 100644 --- a/src/test/java/seedu/address/model/person/UniquePersonListTest.java +++ b/src/test/java/seedu/address/model/person/UniquePersonListTest.java @@ -9,8 +9,10 @@ import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.BOB; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import org.junit.jupiter.api.Test; @@ -166,5 +168,20 @@ public void setPersons_listWithDuplicatePersons_throwsDuplicatePersonException() public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationException() { assertThrows(UnsupportedOperationException.class, () -> uniquePersonList.asUnmodifiableObservableList().remove(0)); + + UniquePersonList anotherUniqueList = new UniquePersonList(); + anotherUniqueList.add( + new Person(new Name("A"), + new Phone("99998888"), + new Email("abc@fmail.com"), + new Address("123 street"), + new Birthday("01/01/2000"), + new HashSet<>(), + new Loan("0"), + new ArrayList<LoanHistory>() + ) + ); + assertThrows(UnsupportedOperationException.class, () + -> anotherUniqueList.asUnmodifiableObservableList().remove(0)); } } diff --git a/src/test/java/seedu/address/model/tag/TagTest.java b/src/test/java/seedu/address/model/tag/TagTest.java index 64d07d79ee2..d1024ea261a 100644 --- a/src/test/java/seedu/address/model/tag/TagTest.java +++ b/src/test/java/seedu/address/model/tag/TagTest.java @@ -1,9 +1,21 @@ package seedu.address.model.tag; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static seedu.address.testutil.Assert.assertThrows; +import java.lang.reflect.Field; + import org.junit.jupiter.api.Test; +import seedu.address.model.person.Loan; +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + public class TagTest { @Test @@ -21,6 +33,60 @@ public void constructor_invalidTagName_throwsIllegalArgumentException() { public void isValidTagName() { // null tag name assertThrows(NullPointerException.class, () -> Tag.isValidTagName(null)); + + assertTrue(Tag.isValidTagName("abcd")); + assertTrue(Tag.isValidTagName("1234")); + assertTrue(Tag.isValidTagName("ab34")); + assertFalse(Tag.isValidTagName("Vice-President")); + assertFalse(Tag.isValidTagName("Vice President")); + assertTrue(Tag.isValidTagName("VicePresident")); + } + + @Test + public void shallowCopy_copyNotSameButEqual() { + Tag tag = new Tag("Operations"); + + Person personA = new PersonBuilder().withName("PersonA").build(); + Person personB = new PersonBuilder().withName("PersonB").build(); + tag.addPerson(personA); + tag.addPerson(personB); + + Tag copy = tag.shallowCopy(); + + assertNotSame(tag, copy); + assertEquals(tag, copy); + + assertEquals(tag.getDeepCopiedPersonList(), copy.getDeepCopiedPersonList()); + } + + @Test + public void getUnmodifiableCopiedPersonList_modifyPerson_doesNotMutateOriginalPerson() { + Person personA = new PersonBuilder().withName("PersonA").build(); + + Tag tag = new Tag("Operations"); + tag.addPerson(personA); + Person reference = tag.getDeepCopiedPersonList().get(0); + + assertEquals(reference, personA); + assertNotSame(reference, personA); + + Field loanField; + try { + loanField = reference.getClass().getDeclaredField("loan"); + } catch (NoSuchFieldException e) { + fail(); + return; + } + loanField.setAccessible(true); + try { + loanField.set(reference, new Loan("355.62")); + } catch (IllegalAccessException | IllegalArgumentException e) { + fail(); + return; + } + loanField.setAccessible(false); + + assertNotEquals(personA.getLoan().getAmount(), reference.getLoan().getAmount()); } } diff --git a/src/test/java/seedu/address/model/tag/UniqueTagMappingTest.java b/src/test/java/seedu/address/model/tag/UniqueTagMappingTest.java new file mode 100644 index 00000000000..5163eae493a --- /dev/null +++ b/src/test/java/seedu/address/model/tag/UniqueTagMappingTest.java @@ -0,0 +1,140 @@ +package seedu.address.model.tag; + +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.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.tag.exceptions.DuplicateTagException; +import seedu.address.model.tag.exceptions.TagNotFoundException; + +public class UniqueTagMappingTest { + + private final UniqueTagMapping uniqueTagMap = new UniqueTagMapping(); + + @Test + public void contains_nullTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagMap.contains((Tag) null)); + assertThrows(NullPointerException.class, () -> uniqueTagMap.contains((String) null)); + } + + @Test + public void contains_tagNotInList_returnsFalse() { + assertFalse(uniqueTagMap.contains(new Tag("Operations"))); + } + + @Test + public void contains_tagInList_returnsTrue() { + Tag tag = new Tag("Operations"); + uniqueTagMap.add(tag); + assertTrue(uniqueTagMap.contains(tag)); + } + + @Test + public void add_nullTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagMap.add(null)); + } + + @Test + public void remove_nullTag_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagMap.remove(null)); + } + + @Test + public void remove_tagDoesNotExist_throwsTagNotFoundException() { + assertThrows(TagNotFoundException.class, () -> uniqueTagMap.remove(new Tag("Operations"))); + } + + @Test + public void remove_existingTag_removesTags() { + uniqueTagMap.add(new Tag("Operations")); + uniqueTagMap.remove(new Tag("Operations")); + UniqueTagMapping expectedUniqueTagMapping = new UniqueTagMapping(); + assertEquals(expectedUniqueTagMapping, uniqueTagMap); + } + + @Test + public void setTags_nullUniqueTagList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagMap.setTags((UniqueTagMapping) null)); + } + + @Test + public void setPersons_uniqueTagList_replacesOwnListWithProvidedUniqueTagList() { + uniqueTagMap.add(new Tag("Operations")); + UniqueTagMapping expectedUniqueTagMapping = new UniqueTagMapping(); + expectedUniqueTagMapping.add(new Tag("Finance")); + uniqueTagMap.setTags(expectedUniqueTagMapping); + assertEquals(expectedUniqueTagMapping, uniqueTagMap); + } + + @Test + public void setTags_nullList_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> uniqueTagMap.setTags((HashMap<String, Tag>) null)); + } + + @Test + public void setTags_list_replacesOwnListWithProvidedList() { + uniqueTagMap.add(new Tag("Operations")); + Map<String, Tag> tagList = Collections.singletonMap("Finance", new Tag("Finance")); + uniqueTagMap.setTags(tagList); + UniqueTagMapping expectedUniqueTagMapping = new UniqueTagMapping(); + expectedUniqueTagMapping.add(new Tag("Finance")); + assertEquals(expectedUniqueTagMapping, uniqueTagMap); + } + + @Test + public void setTags_listWithDuplicateTags_throwsDuplicateTagException() { + Tag uniqueTag = new Tag("Operations"); + + Map<String, Tag> mappingWithDuplicateTags = Map.of( + "Op1", uniqueTag, + "Op2", uniqueTag + ); + + assertThrows(DuplicateTagException.class, + // CHECKSTYLE.OFF: SeparatorWrap + () -> uniqueTagMap.setTags(mappingWithDuplicateTags) + ); + } + + @Test + public void addTags_duplicateTagsAdded_doesNotAddAgain() { + Tag firstTag = new Tag("Backstage"); + Tag secondTag = new Tag("Backstage"); + + uniqueTagMap.add(firstTag); + assertTrue(uniqueTagMap.contains(secondTag)); + + int initialCount = uniqueTagMap.asUnmodifiableObservableMap().size(); + uniqueTagMap.add(secondTag); + assertEquals(initialCount, uniqueTagMap.asUnmodifiableObservableMap().size()); + } + + @Test + public void removeTag_removeByEquivalentTag_removesTagInMapping() { + Tag firstTag = new Tag("Ensemble"); + Tag secondTag = new Tag("Ensemble"); + + int initialCount = uniqueTagMap.asUnmodifiableObservableMap().size(); + uniqueTagMap.add(firstTag); + uniqueTagMap.remove(secondTag); + assertEquals(initialCount, uniqueTagMap.asUnmodifiableObservableMap().size()); + assertFalse(uniqueTagMap.contains(firstTag)); + assertFalse(uniqueTagMap.contains(firstTag.tagName)); + } + + /* TEST REMOVED, ObservableMap::remove is a supported method -- Rui Han + @Test + public void asUnmodifiableObservableMap_modifyMap_throwsUnsupportedOperationException() { + + assertThrows(UnsupportedOperationException.class, () + -> uniqueTagMap.asUnmodifiableObservableMap().remove("0")); + } + */ +} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java index 83b11331cdb..1edd5df69e8 100644 --- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java +++ b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java @@ -14,88 +14,108 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.Loan; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; public class JsonAdaptedPersonTest { private static final String INVALID_NAME = "R@chel"; 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_BIRTHDAY = "2000"; private static final String INVALID_TAG = "#friend"; + private static final String INVALID_LOAN = "abc"; 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_BIRTHDAY = BENSON.getBirthday().toString(); private static final List<JsonAdaptedTag> VALID_TAGS = BENSON.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList()); + private static final String VALID_LOAN = BENSON.getLoan().toString(); + private static final List<JsonAdaptedLoanHistory> VALID_HISTORY = new ArrayList<>(); + + private final List<Tag> tagList = new ArrayList<>(BENSON.getTags()); @Test public void toModelType_validPersonDetails_returnsPerson() throws Exception { JsonAdaptedPerson person = new JsonAdaptedPerson(BENSON); - assertEquals(BENSON, person.toModelType()); + assertEquals(BENSON, person.toModelType(tagList)); } @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_BIRTHDAY, VALID_TAGS, VALID_LOAN, VALID_HISTORY); String expectedMessage = Name.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + assertThrows(IllegalValueException.class, expectedMessage, () -> person.toModelType(tagList)); } @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_BIRTHDAY, VALID_TAGS, VALID_LOAN, VALID_HISTORY); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + assertThrows(IllegalValueException.class, expectedMessage, () -> person.toModelType(tagList)); } @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_BIRTHDAY, VALID_TAGS, VALID_LOAN, VALID_HISTORY); String expectedMessage = Phone.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + assertThrows(IllegalValueException.class, expectedMessage, () -> person.toModelType(tagList)); } @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_BIRTHDAY, VALID_TAGS, VALID_LOAN, VALID_HISTORY); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + assertThrows(IllegalValueException.class, expectedMessage, () -> person.toModelType(tagList)); } @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_BIRTHDAY, VALID_TAGS, VALID_LOAN, VALID_HISTORY); String expectedMessage = Email.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + assertThrows(IllegalValueException.class, expectedMessage, () -> person.toModelType(tagList)); } @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_BIRTHDAY, VALID_TAGS, VALID_LOAN, VALID_HISTORY); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + assertThrows(IllegalValueException.class, expectedMessage, () -> person.toModelType(tagList)); } @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_BIRTHDAY, VALID_TAGS, VALID_LOAN, VALID_HISTORY); String expectedMessage = Address.MESSAGE_CONSTRAINTS; - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + assertThrows(IllegalValueException.class, expectedMessage, () -> person.toModelType(tagList)); } @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_BIRTHDAY, VALID_TAGS, VALID_LOAN, VALID_HISTORY); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); - assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + assertThrows(IllegalValueException.class, expectedMessage, () -> person.toModelType(tagList)); } @Test @@ -103,8 +123,27 @@ public void toModelType_invalidTags_throwsIllegalValueException() { List<JsonAdaptedTag> invalidTags = new ArrayList<>(VALID_TAGS); invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); - assertThrows(IllegalValueException.class, person::toModelType); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_BIRTHDAY, invalidTags, VALID_LOAN, VALID_HISTORY); + assertThrows(IllegalValueException.class, () -> person.toModelType(tagList)); + } + + @Test + public void toModelType_invalidLoan_throwsIllegalValueException() { + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_BIRTHDAY, VALID_TAGS, INVALID_LOAN, VALID_HISTORY); + String expectedMessage = Loan.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, () -> person.toModelType(tagList)); + } + + @Test + public void toModelType_nullLoan_throwsIllegalValueException() { + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_BIRTHDAY, VALID_TAGS, null, VALID_HISTORY); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Loan.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, () -> person.toModelType(tagList)); } } diff --git a/src/test/java/seedu/address/testutil/EditNoteDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditNoteDescriptorBuilder.java new file mode 100644 index 00000000000..04d46c7eafc --- /dev/null +++ b/src/test/java/seedu/address/testutil/EditNoteDescriptorBuilder.java @@ -0,0 +1,70 @@ +package seedu.address.testutil; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.logic.commands.EditNoteCommand.EditNoteDescriptor; +import seedu.address.model.note.Content; +import seedu.address.model.note.Note; +import seedu.address.model.note.Title; +import seedu.address.model.tag.Tag; + +/** + * A utility class to help with building EditNoteDescriptor objects. + */ +public class EditNoteDescriptorBuilder { + + private EditNoteDescriptor descriptor; + + public EditNoteDescriptorBuilder() { + descriptor = new EditNoteDescriptor(); + } + + public EditNoteDescriptorBuilder(EditNoteDescriptor descriptor) { + this.descriptor = new EditNoteDescriptor(descriptor); + } + + /** + * Returns an {@code EditNoteDescriptor} with fields containing {@code note}'s details + */ + public EditNoteDescriptorBuilder(Note note) { + descriptor = new EditNoteDescriptor(); + descriptor.setTitle(note.getTitle()); + descriptor.setContent(note.getContent()); + descriptor.setTags(note.getTags()); + } + + /** + * Sets the {@code Title} of the {@code EditNoteDescriptor} that we are building. + */ + public EditNoteDescriptorBuilder withTitle(String title) { + descriptor.setTitle(new Title(title)); + return this; + } + + /** + * Sets the {@code Content} of the {@code EditNoteDescriptor} that we are building. + */ + public EditNoteDescriptorBuilder withContent(String content) { + descriptor.setContent(new Content(content)); + return this; + } + + /** + * Parses the {@code tags} into a {@code Set<Tag>} and set it to the {@code EditNoteDescriptor} + * that we are building. + */ + public EditNoteDescriptorBuilder withTags(String... tags) { + Set<Tag> tagSet = Stream.of(tags).map(Tag::new).collect(Collectors.toSet()); + descriptor.setTags(tagSet); + return this; + } + + public EditNoteDescriptor build() { + return descriptor; + } + + + +} diff --git a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java index 4584bd5044e..e9ded6003ad 100644 --- a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java +++ b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java @@ -6,6 +6,7 @@ import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; @@ -36,6 +37,7 @@ public EditPersonDescriptorBuilder(Person person) { descriptor.setPhone(person.getPhone()); descriptor.setEmail(person.getEmail()); descriptor.setAddress(person.getAddress()); + descriptor.setBirthday(person.getBirthday()); descriptor.setTags(person.getTags()); } @@ -71,6 +73,14 @@ public EditPersonDescriptorBuilder withAddress(String address) { return this; } + /** + * Sets the {@code Birthday} of the {@code EditPersonDescriptor} that we are building. + */ + public EditPersonDescriptorBuilder withBirthday(String birthday) { + descriptor.setBirthday(new Birthday(birthday)); + return this; + } + /** * Parses the {@code tags} into a {@code Set<Tag>} and set it to the {@code EditPersonDescriptor} * that we are building. diff --git a/src/test/java/seedu/address/testutil/NoteBuilder.java b/src/test/java/seedu/address/testutil/NoteBuilder.java new file mode 100644 index 00000000000..eefa795a5a8 --- /dev/null +++ b/src/test/java/seedu/address/testutil/NoteBuilder.java @@ -0,0 +1,70 @@ +package seedu.address.testutil; + +import java.util.HashSet; +import java.util.Set; + +import seedu.address.model.note.Content; +import seedu.address.model.note.Note; +import seedu.address.model.note.Title; +import seedu.address.model.tag.Tag; +import seedu.address.model.util.SampleDataUtil; + +/** + * A utility class to help with building Note objects. + */ +public class NoteBuilder { + + public static final String DEFAULT_TITLE = "Todo"; + public static final String DEFAULT_CONTENT = "Collect money"; + + private Title title; + private Content content; + private Set<Tag> tags; + + /** + * Creates a {@code NoteBuilder} with the default details. + */ + public NoteBuilder() { + title = new Title(DEFAULT_TITLE); + content = new Content(DEFAULT_CONTENT); + tags = new HashSet<>(); + } + + /** + * Initializes the NoteBuilder with the data of {@code noteToCopy}. + */ + public NoteBuilder(Note noteToCopy) { + title = noteToCopy.getTitle(); + content = noteToCopy.getContent(); + tags = new HashSet<>(noteToCopy.getTags()); + } + + /** + * Sets the {@code Title} of the {@code Note} that we are building. + */ + public NoteBuilder withTitle(String title) { + this.title = new Title(title); + return this; + } + + /** + * Sets the {@code Content} of the {@code Note} that we are building. + */ + public NoteBuilder withContent(String content) { + this.content = new Content(content); + return this; + } + + /** + * Parses the {@code tags} into a {@code Set<Tag>} and set it to the {@code Note} that we are building. + */ + public NoteBuilder withTags(String ... tags) { + this.tags = SampleDataUtil.getTagSet(tags); + return this; + } + + public Note build() { + return new Note(title, content, tags); + } + +} diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java index 6be381d39ba..0c835e8004d 100644 --- a/src/test/java/seedu/address/testutil/PersonBuilder.java +++ b/src/test/java/seedu/address/testutil/PersonBuilder.java @@ -1,10 +1,15 @@ package seedu.address.testutil; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; +import seedu.address.model.person.Loan; +import seedu.address.model.person.LoanHistory; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -20,12 +25,18 @@ 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_BIRTHDAY = "01/01/2000"; + public static final String DEFAULT_LOAN = "0"; + private Name name; private Phone phone; private Email email; private Address address; + private Birthday birthday; private Set<Tag> tags; + private Loan loan; + private List<LoanHistory> history; /** * Creates a {@code PersonBuilder} with the default details. @@ -35,7 +46,10 @@ public PersonBuilder() { phone = new Phone(DEFAULT_PHONE); email = new Email(DEFAULT_EMAIL); address = new Address(DEFAULT_ADDRESS); + birthday = new Birthday(DEFAULT_BIRTHDAY); tags = new HashSet<>(); + loan = new Loan(DEFAULT_LOAN); + history = new ArrayList<>(); } /** @@ -46,7 +60,10 @@ public PersonBuilder(Person personToCopy) { phone = personToCopy.getPhone(); email = personToCopy.getEmail(); address = personToCopy.getAddress(); + birthday = personToCopy.getBirthday(); tags = new HashSet<>(personToCopy.getTags()); + loan = personToCopy.getLoan(); + history = personToCopy.getHistory(); } /** @@ -89,8 +106,24 @@ public PersonBuilder withEmail(String email) { return this; } + /** + * Sets the {@code Birthday} of the {@code Person} that we are building. + */ + public PersonBuilder withBirthday(String birthday) { + this.birthday = new Birthday(birthday); + return this; + } + + /** + * Sets the {@code Loan} of the {@code Person} that we are building. + */ + public PersonBuilder withLoan(String loan) { + this.loan = new Loan(loan); + return this; + } + public Person build() { - return new Person(name, phone, email, address, tags); + return new Person(name, phone, email, address, birthday, tags, loan, new ArrayList<LoanHistory>()); } } diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java index 90849945183..90f4ed38384 100644 --- a/src/test/java/seedu/address/testutil/PersonUtil.java +++ b/src/test/java/seedu/address/testutil/PersonUtil.java @@ -1,6 +1,7 @@ package seedu.address.testutil; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; 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_PHONE; @@ -34,6 +35,7 @@ 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_BIRTHDAY + person.getBirthday().value + " "); person.getTags().stream().forEach( s -> sb.append(PREFIX_TAG + s.tagName + " ") ); @@ -49,6 +51,7 @@ 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.getBirthday().ifPresent(birthday -> sb.append(PREFIX_BIRTHDAY).append(birthday.value).append(" ")); if (descriptor.getTags().isPresent()) { Set<Tag> tags = descriptor.getTags().get(); if (tags.isEmpty()) { diff --git a/src/test/java/seedu/address/testutil/TypicalIndexes.java b/src/test/java/seedu/address/testutil/TypicalIndexes.java index 1e613937657..1b75a610c71 100644 --- a/src/test/java/seedu/address/testutil/TypicalIndexes.java +++ b/src/test/java/seedu/address/testutil/TypicalIndexes.java @@ -9,4 +9,7 @@ public class TypicalIndexes { public static final Index INDEX_FIRST_PERSON = Index.fromOneBased(1); public static final Index INDEX_SECOND_PERSON = Index.fromOneBased(2); public static final Index INDEX_THIRD_PERSON = Index.fromOneBased(3); + public static final Index INDEX_FIRST_NOTE = Index.fromOneBased(1); + public static final Index INDEX_SECOND_NOTE = Index.fromOneBased(2); + public static final Index INDEX_THIRD_NOTE = Index.fromOneBased(3); } diff --git a/src/test/java/seedu/address/testutil/TypicalNotes.java b/src/test/java/seedu/address/testutil/TypicalNotes.java new file mode 100644 index 00000000000..1bd0bd813e1 --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalNotes.java @@ -0,0 +1,70 @@ +package seedu.address.testutil; + +import static seedu.address.logic.commands.CommandTestUtil.VALID_CONTENT_CLUB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_CONTENT_MEETING; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TITLE_CLUB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TITLE_MEETING; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.model.AddressBook; +import seedu.address.model.note.Note; +import seedu.address.model.tag.Tag; + +/** + * A utility class containing a list of {@code Note} objects to be used in tests. + */ +public class TypicalNotes { + + public static final Note ACCOUNTING = new NoteBuilder().withTitle("Accounting") + .withContent("Do accounting").withTags("a").build(); + + public static final Note BUY = new NoteBuilder().withTitle("Buy") + .withContent("Buy food").withTags("b").build(); + + public static final Note CHARITY = new NoteBuilder().withTitle("Charity Event") + .withContent("Charity event").withTags("c").build(); + + public static final Note DONATE = new NoteBuilder().withTitle("Donate Event") + .withContent("Donate food").withTags("d").build(); + + public static final Note ELECTION = new NoteBuilder().withTitle("Election Event") + .withContent("Elect new club president").withTags("e").build(); + + // Manually added + public static final Note FOOTBALL = new NoteBuilder().withTitle("Football") + .withContent("Football practice").build(); + + // Manually added - Note's details found in {@code CommandTestUtil} + public static final Note MEETING = new NoteBuilder().withTitle(VALID_TITLE_MEETING) + .withContent(VALID_CONTENT_MEETING).build(); + + public static final Note CLUB = new NoteBuilder().withTitle(VALID_TITLE_CLUB) + .withContent(VALID_CONTENT_CLUB).build(); + + public static final String KEYWORD_MATCHING_EVENT = "Event"; // A keyword that matches EVENT + + private TypicalNotes() {} // prevents instantiation + + /** + * Returns an {@code AddressBook} with all the typical notes. + */ + public static AddressBook getTypicalAddressBook() { + AddressBook ab = new AddressBook(); + for (Note note : getTypicalNotes()) { + ab.addNote(note); + for (Tag tag : note.getTags()) { + ab.addTag(tag); + } + } + return ab; + } + + public static List<Note> getTypicalNotes() { + return new ArrayList<>(Arrays.asList(ACCOUNTING, BUY, CHARITY, DONATE, ELECTION)); + } + + +} diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java index fec76fb7129..1c6b22b1d90 100644 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ b/src/test/java/seedu/address/testutil/TypicalPersons.java @@ -17,6 +17,7 @@ import seedu.address.model.AddressBook; import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; /** * A utility class containing a list of {@code Person} objects to be used in tests. @@ -25,28 +26,31 @@ 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").withBirthday("02/02/2000") .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(); + .withEmail("johnd@example.com").withPhone("98765432").withBirthday("01/01/2000") + .withTags("owesMoney").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").withAddress("wall street").withBirthday("03/03/2000").build(); public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533") - .withEmail("cornelia@example.com").withAddress("10th street").withTags("friends").build(); - public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("9482224") - .withEmail("werner@example.com").withAddress("michegan ave").build(); - public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427") - .withEmail("lydia@example.com").withAddress("little tokyo").build(); + .withEmail("cornelia@example.com").withAddress("10th street").withBirthday("11/11/2000") + .withTags("friends").build(); + public static final Person ELLE = new PersonBuilder().withName("Elle Meyer") + .withPhone("9482224").withBirthday("04/04/2000") + .withEmail("werner@example.com").withAddress("michegan ave").withLoan("-10.0").build(); + public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz") + .withPhone("9482427").withBirthday("05/05/2000") + .withEmail("lydia@example.com").withAddress("little tokyo").withLoan("50.0").build(); public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("9482442") - .withEmail("anna@example.com").withAddress("4th street").build(); + .withEmail("anna@example.com").withAddress("4th street").withBirthday("06/06/2000").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").withBirthday("07/07/2000").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").withBirthday("08/08/2000").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) @@ -66,6 +70,9 @@ public static AddressBook getTypicalAddressBook() { AddressBook ab = new AddressBook(); for (Person person : getTypicalPersons()) { ab.addPerson(person); + for (Tag tag : person.getTags()) { + ab.addTag(tag); + } } return ab; }