diff --git a/.github/ISSUE_TEMPLATE/bug-report-template.md b/.github/ISSUE_TEMPLATE/bug-report-template.md new file mode 100644 index 00000000000..1210d580acf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-template.md @@ -0,0 +1,22 @@ +--- +name: Bug Report Template +about: Template for Bug Reports +title: '' +labels: '' +assignees: '' +--- + +## Details + + +## Steps to reproduce + + +## Expected behavior + + +## Actual behavior + + +## Screenshots + diff --git a/.github/ISSUE_TEMPLATE/user-story-template.md b/.github/ISSUE_TEMPLATE/user-story-template.md new file mode 100644 index 00000000000..5c4fd85d217 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/user-story-template.md @@ -0,0 +1,14 @@ +--- +name: User stories Template +about: Template for User stories +title: '' +labels: '' +assignees: '' +--- + +## User stories +- [Insert user stories here] + +## Details +- [Insert details here] + - Format: [insert format if necessary] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..95d04a5df85 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +## Description + + + +## Context + + + +## Checklist + +- [ ] I have self-reviewed my changes. +- [ ] I have added JavaDocs to all relevant methods. +- [ ] I have updated the documentation: + - User Guide Table of Contents, Description and Summary. + - Developer Guide, if applicable. +- [ ] I have ran `./gradlew test` or `./gradlew check` and all checks pass. +- [ ] I have updated my project portfolio with what I have contributed. diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..eeff3edabd9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /.gradle/ /build/ src/main/resources/docs/ +/bin/ # IDEA files /.idea/ @@ -21,3 +22,6 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ + +# jar files +*.jar diff --git a/README.md b/README.md index 13f5c77403f..fce41d3881c 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ -[![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/AY2324S1-CS2103T-T12-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S1-CS2103T-T12-2/tp/actions) +[![codecov](https://codecov.io/gh/AY2324S1-CS2103T-T12-2/tp/graph/badge.svg?token=MDL2TF28EC)](https://codecov.io/gh/AY2324S1-CS2103T-T12-2/tp) +## FumbleLog +![Ui](docs/images/Ui.png) -![Ui](docs/images/Ui.png) +FumbleLog is a **productivity desktop application** built to for **NUS Computing students** to help you manage contacts and track events. +It is designed to be an easy-to-use, one-stop platform for all your scheduling needs. -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. +* If you are interested in using FumbleLog, head over to the [_Quick Start_ section of the **User Guide**](docs/UserGuide.md#quick-start). +* If you are interested about developing FumbleLog, the [**Developer Guide**](docs/DeveloperGuide.md) is a good place to start. + +The project simulates an ongoing software project for a desktop application (called _FumbleLog_) 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. + +* For the detailed documentation of this project, see the **[FumbleLog Product Website](https://ay2324s1-cs2103t-t12-2.github.io/tp/)**. +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). diff --git a/build.gradle b/build.gradle index a2951cc709e..7caf582dda4 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) @@ -66,7 +70,7 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'fumblelog.jar' } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..6e94250401d 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,56 @@ 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 visit our project repository at our [organisation github](https://github.com/AY2324S1-CS2103T-T12-2). ## Project team -### John Doe +### Lee Zheng Jing - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/leezhengjing)] +[[portfolio](team/leezhengjing.md)] -* Role: Project Advisor +* Role: Developer +* Responsibilities: Data -### Jane Doe +### Kurt Lee Yi Jie - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/Kurtyjlee)] +[[portfolio](team/kurtyjlee.md)] -* Role: Team Lead -* Responsibilities: UI +* Role: Developer +* Responsibilities: Back-end and QA -### Johnny Doe +### Leong Yu Jun Nicholas - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](https://github.com/nicleongyj)] +[[portfolio](team/nicleongyj.md)] * Role: Developer * Responsibilities: Data -### Jean Doe +### Guo Yuheng - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/DistractedCat)] +[[portfolio](team/distractedcat.md)] * Role: Developer * Responsibilities: Dev Ops + Threading -### James Doe +### Lai Wei Zhong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/Ken-Lai)] +[[portfolio](team/ken-lai.md)] * Role: Developer * Responsibilities: UI diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 8a861859bfd..0dae07c03c3 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,6 +2,7 @@ layout: page title: Developer Guide --- +## Table of Contents * Table of Contents {:toc} @@ -9,7 +10,7 @@ title: Developer Guide ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) -------------------------------------------------------------------------------------------------------------------- @@ -19,6 +20,8 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md). -------------------------------------------------------------------------------------------------------------------- +
+ ## **Design**
@@ -51,7 +54,7 @@ The bulk of the app's work is done by the following four components: **How the architecture components interact with each other** -The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete_person 1`. @@ -66,13 +69,15 @@ For example, the `Logic` component defines its API in the `Logic.java` interface The sections below give more details of each component. +
+ ### UI component The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) ![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`, `EventListPanel`, `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) @@ -81,7 +86,11 @@ 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 `Event` object residing in the `Model`. + +[Scroll back to Table of Contents](#table-of-contents) + +
### Logic component @@ -91,17 +100,17 @@ Here's a (partial) class diagram of the `Logic` component: -The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example. +The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete_person 1")` API call as an example. -![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) +![Interactions Inside the Logic Component for the `delete_person 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.
How the `Logic` component works: -1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. +1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `DeletePersonCommandParser`) and uses it to parse the command. +1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeletePersonCommand`) which is executed by the `LogicManager`. 1. The command can communicate with the `Model` when it is executed (e.g. to delete a person). 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. @@ -110,50 +119,432 @@ Here are the other classes in `Logic` (omitted from the class diagram above) tha How the parsing works: -* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. -* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. +* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddPersonCommand`) which the `AddressBookParser` returns back as a `Command` object. +* All `XYZCommandParser` classes (e.g., `AddPersonCommandParser`, `DeletePersonCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. + +[Scroll back to Table of Contents](#table-of-contents) + +
### Model component **API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) - + +[Scroll back to Table of Contents](#table-of-contents) 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`(which are contained in a `UniquePersonList` object) and `Event` objects (which are contained in a `EventList` 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 the currently 'selected' `Event` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. +* stores a `Logger` object that is used to log messages of the application's behaviour. * 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.
- - - -
+ +[Scroll back to Table of Contents](#table-of-contents) ### Storage component **API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) - + + +
The `Storage` component, * can save both address book data and user preference data in JSON format, and read them back into corresponding objects. * inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). * depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) +[Scroll back to Table of Contents](#table-of-contents) + ### Common classes Classes used by multiple components are in the `seedu.addressbook.commons` package. +[Scroll back to Table of Contents](#table-of-contents) + +-------------------------------------------------------------------------------------------------------------------- + +
+ +## **Flow of Program Execution** + +The way the user interacts with the program is illustrated as follows. + +![FlowOfProgram](images/CommandFlowActivityDiagram.png) + +The following is a (partial) explanation of the flow of events: +1. The user makes their command by issuing a command in the CommandBox. +2. The command is parsed by the Parser and the corresponding Command object is created. +3. The Command object is executed. +4. The Command is executed and returns a CommandResult. +5. The CommandResult is passed to the UI component to be displayed to the user. + +More details are captured in the diagram above. + +If the command involves the changing of Models, the Models are updated accordingly at stage 3 during the execution process. +Changes to the models will also be reflected in Storage in the backend. +These model changes will also be reflected in the Ui (e.g when a Person or an Event is changed). + +The errors during process and execution are also handled accordingly by displaying an error message to the user. + +The object diagram below illustrates a possible state of the Models after some commands have been executed. + +![Model state](images/AddEventObjectDiagram.png) + +Assume that the user just added a `Meeting` which is a subtype of `Event`. They supplied the meeting with a name, a start date and a start time. The user also added some previous events and persons as shown in the diagram. + +This shows how the Models are stored for use in the program. + +Note that even though EventList stores a list of Events, currently only Meetings (a subtype of Event) are implemented. This is to allow for future extensibility of the program. + +[Scroll back to Table of Contents](#table-of-contents) + -------------------------------------------------------------------------------------------------------------------- + +
+ ## **Implementation** +### Commands +This section explains the general implementation of all commands. + +The following activity diagrams shows the overall flow of events that the user will experience. + This section describes some noteworthy details on how certain features are implemented. +#### Parser Commands +This section explains the implementation and execution of commands that have their own specific parser. + +Below is the sequence diagram for the execution of these commands (denoted by `XYZCommand`) after user input is sent to `LogicManager`. The execution of each of the command has been omitted due to their inherent differences and will be covered in their respective command sections below. + +![Command Parser Sequence Diagram](images/CommandsParserSequenceDiagram.png) + +Step 1: +The user enters a command with the necessary parameters which is then passed to the `LogicManager`. + +Step 2: +The `LogicManager` calls `AddressBookParser::parseCommand` for it to identify the type of command. + +Step 3: +The `AddressBookParser` parses the user input and creates a command parser for that specific command. (denoted by `XYZCommandParser`) + +Step 4: +The command parser is returned to the `AddressBookParser` which then calls `XYZCommandParser::parse` to parse the additional parameters. + +Step 5: +The `XYZCommandParser` creates its respective command object (denoted by `XYZCommand`) and returns it to `LogicManager`. + +Step 6: +The `LogicManager` calls `XYZCommand::execute` where the interaction between the command and the model is handled. + +Step 7: +The `XYZCommand` creates a successful `CommandResult` and returns it to the UI. + +[Scroll back to Table of Contents](#table-of-contents) + +### Ability to add persons +This section explains the implementation of the Add Task feature via the `add_person` command. +The `AddPersonCommand` causes the specified `Person` to be added to the Persons List in the application. +There is only one compulsory field which is the name of the Person. There are several optional fields such as the phone number, email address, address, birthday and remark of the Person. + +Below is the sequence diagram outlining the execution of `AddTaskCommand`. + +#### Implementation details + +The `add_person` feature involves creating a new `Person` object with optional fields and adding it to FumbleLog. + +This is done using `AddPersonCommand` which implements the `Command` interface. The `AddPersonCommand` is then executed by `LogicManager` which calls `ModelManager` to add the person to the address book. + +As a result, the existing `Person` class in AB3's implementation is enhanced to have the capacity of storing optional fields. +Below is a class diagram of the enhanced 'Person' class: + +PersonClassDiagram + +The `Person` object is now composed of the following optional attributes due to the refactored `add` feature: + +* `Name`: The name of the person. Compulsory field. +* `Phone`: The phone number of the person. Optional field. +* `Email`: The email address of the person. Optional field. +* `Address`: The address of the person. Optional field. +* `Birthday`: The birthday of the person. Optional field. +* `Remark`: The remark of the person. Optional field. +* `Group`: The groups that the person is associated with. Optional field. + +The [**`java.util.Optional`**](https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html) class is used to represent the optional attributes of the `Person` object. + +To add a person, the user must specify the name of the person using the `n/` prefix. The user can then specify the optional attributes of the person using the following prefixes: + + + +Except for the `Name`, all the fields given to the `add_person` command are optional. + + + +The flow for the `add_person` command is described by the following sequence diagram: + +AddPersonSequenceDiagram2 + +Step 1: +The `LogicManager` invokes `AddPersonCommand::execute`, which in turn calls `Model::addPerson`. + +Step 2: +The `Model` will invoke `addPerson` in `AddressBook`, which in turn calls `add` in `UniquePersonList` to add the person to the list. + +Step 3: +The `AddPersonCommand` then continues its execution as defined by [this](#parser-commands) sequence diagram. + +#### Feature details +1. The application will validate the arguments supplied by the user; whether the `Name` is unique and supplied, and whether the optional fields follow the correct format. +2. If the arguments are invalid, an error message will be shown to the user and prompts the user for a corrected input. +3. If the arguments are valid, a `Person` object will be created with the fields supplied and stored in FumbleLog. + +#### Design Considerations + +**Aspect: Generic Design** + +The original implementation of AB3's `Person` class is refactored to have the capacity of storing optional fields. This is done by using the `java.util.Optional` class to represent the optional attributes of the `Person` object. +Furthermore, we have added additional fields into the `Person` class to allow users to store more information about the person, such as their birthday. + +As the original `add` command already exists in AB3, this feature can be implemented by enhancing the `add` command. In FumbleLog, the `add` command is further changed to `add_person`. + +Furthermore, we accounted for empty/null inputs in the optional fields by generating a NULL_INSTANCE for the optional fields when the user does not specify the optional fields. This design decision allowed us to easily check +for empty/null inputs in the Person object by checking if the optional field is not equal to the NULL_INSTANCE, instead of doing null pointer and empty string checks. + +* **Alternative 1 (current choice):** Enhance the existing `add_person` command. + * Pros: + * Easier to implement. + * Reuses the logic for the `add_person` command. + * Cons: + * Have to account for empty/null inputs in the optional fields when saving the data and testing it + * Have to account for empty/null inputs in the optional fields when displaying the data +* **Alternative 2**: Create a new `add_person_optional` command. + * Pros: + * Do not have to account for empty/null inputs in the optional fields when saving the data and testing it + * Cons: + * Inconveniences the user as they have to remember a new command to add a person with optional fields. + +[Scroll back to Table of Contents](#table-of-contents) + +### Ability to delete persons + +This section explains the implementation of the Delete Person feature via the `delete_person` command. The `DeletePersonCommand` causes the specified `Person` identified using the `Index` to be deleted from the UniquePerson List in the application. There is one compulsory field which is the Index of the Person to delete. + +Below is the sequence diagram outlining the execution of `DeletePersonCommand`. + +![DeletePersonCommand Sequence Diagram](images/DeletePersonSequenceDiagram.png) + +Step 1: +The `LogicManager` invokes `DeletePersonCommand::execute`, which in turn calls `Model::deletePerson`. + +Step 2: +The `Model` will invoke `removePerson` in `AddressBook`, which in turn calls `remove` in `UniquePersonList` to remove it from the list. + +Step 3: +The `DeletePersonCommand` then continues its execution as defined by [this](#parser-commands) sequence diagram. + +#### Design Considerations +**Aspect: How we execute the DeletePersonCommand:** +Similar to the `AddPersonCommand`, the main considerations for this command is related to the way that the model is stored. + +[Scroll back to Table of Contents](#table-of-contents) + +### Ability to track events + +This subsection details of how the `Event` class is implemented. + +#### Implementation + +For this feature, the `Event` class is implemented to store the event's name, date, +start time, end time, persons involved and groups involved. + +EventClassDiagram + +#### Design considerations + +- Events stores a list of `Name` and a list of `Group` that are involved in the event. +This is to facilitate the ability to track persons and groups involved in the event. +The `Name` class is used to represent the name of the person involved in the event, as names are unique in the `UniquePersonList`. +- We have also made `Event` an abstract class so as to increase extensibility of FumbleLog in the future. For now, when an event is created (i.e. using the `AddEventCommand`), it defaults to adding a `Meeting` into FumbleLog's `Event` List. Future support for other kinds of `Event` can be possible (i.e. Recurring event) by directly inheriting from `Event`. +- To track events, we implement an `EventList` to store all events to be displayed in FumbleLog. + +[Scroll back to Table of Contents](#table-of-contents) + +### Ability to assign/unassign persons from an event + +#### Implementation + +- The ability to assign a `Person` to an `Event` is facilitated by `ModelManager`. +- Each `Event` stores a list of person assigned to it. +The person(s) are represented by their `Name` stored in FumbleLog. +This is because the `Name` is the only unique identifier for a person. +- When a person is assigned to an event, the person's `Name` is added to the `Event`'s list +of assigned persons. + - When a `Person` is unassigned from an `Event`, the person's `Name` is +removed from the `Event`'s list of assigned persons. + - When a person's `Name` is modified, the change is also reflected in the event(s) + that they are previously assigned to. +- Users can assign multiple names to an event by using multiple `n/` identifiers following +with the `Name` specified. The `ModelManager` will perform checks on whether the names supplied are valid, +i.e the `Name` currently exists in FumbleLog. +- When editing the event, specifying `n/` with a `Name` will append this new name to the current list rather than replace the previous names. +This is to facilitate the user to assign more persons without accidentally deleting the previous persons assigned. + - To un-assign a `Person`, the user must manually specify `u/` with the `Name` to un-assign the `Person` from the `Event`. + - The `ModelManager` will perform checks on whether the names supplied are valid, + i.e the `Name` currently exists in FumbleLog, and that the `Name` is currently assigned to the `Event`. + +[Scroll back to Table of Contents](#table-of-contents) + +### Ability to assign groups to an event + +#### Implementation + +- The ability to assign a `Group` to an `Event` is facilitated by `ModelManager`. +- Each `Event` stores a list of `Groups` assigned to it. + - That is, when a `Group` is assigned to an `Event`, a `Group`object is stored a `Set` of `Group`. + - When un-assigned, the corresponding groups are then removed from the `Set`. +- To make it easier for the users to assign groups, this action is done through the `AddEventCommand` and `EditEventCommand`, with the `g/` prefix. + - Users can assign multiple groups to an event by using multiple `g/` identifiers following + with the `Group` specified. The `ModelManager` will perform checks on whether the names supplied are valid, + i.e the `Group` currently exists in FumbleLog. +- To un-assign a `Group`, the user must manually specify `ug/` with the `Group` to un-assign it from the `Event`. + - The `ModelManager` will perform checks on whether the groups supplied are valid, + i.e the `Group` currently exists in FumbleLog, and that the `Group` is currently assigned to the `Event`. +- With the `Group` name, person(s) with that specific `Group` in their `Group` list is displayed with the `Event`. +- After a `Group` has been assigned to an `Event`, all `Person` in that `Group` will be displayed with the `Event` on FumbleLog. + +A successful `EditEventCommand` that assigns groups should look like this: + +AssignGroup + +This is a possible object representation of an `Event` with a `Group` and a `Person` assigned to it. + +AssignGroup + +- In this object diagram, the `Event`, `TP meeting` has a `Person`, John, assigned to it and a `Group` CS2103T assigned to it. +- In this case, TP meeting only stores these information and will use its respectively `Person` list and `Group` list to display: + - John as assigned to it + - Bob and Alice as assigned to it within a group. + +#### Design considerations + +- When adding and displaying groups, persons that has been added individually previously will be displayed twice. To counter that, checks are done to ensure that +when a group is added, duplicate persons will be deleted from the individual persons list +- A person can belong to multiple groups, due to the multiplicity between groups and persons. In this case, we allow multiple persons to be displayed, as it is clear which group they belong to. +- As the persons are searched by their group name only when displaying, adding new persons, editing and deleting persons is simple as the component just reloads and searches for everybody in the groups again. + +[Scroll back to Table of Contents](#table-of-contents) + +### Ability to find persons and events + +The `find_XYZ` commands in our application displays persons and/or events that fit the keyword(s). + +#### Implementation + +The original `find` command has been split into three new commands, `find_person`, `find_event`, and `find_all`, which +are collectively called `find_XYZ` commands. +* `find_person` command allows the user to find persons with fitting name or groups. +* `find_event` command allows the user to find events with fitting name, assigned persons and groups, or person under assigned groups. +* `find_all` command allows the user to find persons and events, which includes the criteria mentioned above. + +The `find_person` command involves checking the current full list of persons and filtering out persons with fitting names +or groups. This is done using `PersonNameOrGroupContainsKeywordsPredicate`, which enhanced from the original +`NameContainsKeywordsPredicate` class. The predicate is then passed to `Model#updateFilteredPersonList(Predicate predicate)`. + +Meanwhile, the `find_event` command involves checking the current full list of events and filtering out event with fitting names, groups or persons. +This is done using `EventNameOrGroupContainsKeywordsPredicate`, which also enhanced from `NameContainsKeywordsPredicate` +class. The predicate is then passed to `Model#updateFilteredEventList(Predicate predicate)`. + +`find_all` command covers the mechanism of both commands mentioned above, which uses both predicates and calls both functions in `Model`. + +As a result, the `ObservableList` and/or `ObservableList` are updated with the filtered lists of persons and events. +The `UI` component is notified of these new changes to the lists and updates the UI accordingly, which will show the updated lists. + +#### Feature details + +1. The `find_XYZ` commands can accept one or more parameter `keyword` for searching persons and/or events. +2. A `PersonNameOrGroupContainsKeywordsPredicate` and/or `EventNameOrGroupContainsKeywordsPredicate` will be created and a `FindXYZ` command will be created with the predicates. +3. The `FindXYZ` command will then be executed and the `UI` will be updated with the filtered lists of persons and/or events. + +The flow for the `find_XYZ` commands are described by the following sequence diagrams: + +#### `find_person` +![FindPersonSequenceDiagram](images/FindPersonSequenceDiagram.png) + +#### `find_event` +![FindEventSequenceDiagram](images/FindEventSequenceDiagram.png) + +#### `find_all` +![FindAllSequenceDiagram](images/FindAllSequenceDiagram.png) + +#### General design considerations + +**Aspect: Keyword target differentiation** + +- **Alternative 1 (Current choice): Find all persons that fits the keyword.** + - Pros: + - Easier to implement. + - Cons: + - Might get unwanted results, which decreases overall experience. +- **Alternative 2: Differentiate the target of keyword with syntax.** + - Pros: + - User can find exact person or group. + - Cons: + - Adding constraint the original command by requiring syntax, which may cause convenience. + +[Scroll back to Table of Contents](#table-of-contents) + +### Remind feature + +The `remind` command in our application displays a birthdays and events that will happen within a specified number of days. + +#### Implementation + +The `remind` feature involves checking the current filtered list of persons and events and filtering out persons with birthdays and events with starting date +that are within the specified number of days. This is done using `BirthdayWithinDaysPredicate` and `EventWithinDaysPredicate` which implements the `Predicate` interface. These predicates are passed +to `Model#updateFilteredPersonList(Predicate predicate)` and `Model#updateFilteredEventList(Predicate predicate)` respectively. + +As a result, the `ObservableList` and `ObservableList` are updated with the filtered lists of persons and events respectively. +The `UI` component is notified of these new changes to the lists and updates the UI accordingly, which will show the updated persons and events. + +The `remind` command is implemented this way as it reuses the logic for the `find` command where it utilises the `Model` component to update the current list of persons based on the given predicate. +Instead of filtering out persons based on names, the `BirthdayWithinDaysPredicate` filters out persons based on their birthdays and the `EventWithinDaysPredicate` filters out events based on their starting dates. + +The flow for the `remind` command is described by the following sequence diagram: + +![RemindSequenceDiagram](images/RemindSequenceDiagram.png) + +#### Feature details +1. The `remind` command can accept an optional parameter `days` which specifies the number of days to search for birthdays and events. If `days` is not specified, the default value of 7 days will be used. +2. The application will validate the argument `days` to ensure that it is a positive integer. If it is not, an error message will be shown to the user and prompts the user for a corrected input. +3. If it is a valid input, a `BirthdayWithinDaysPredicate` and `EventWithinDaysPredicate` will be created and a `Remind` command will be created with the predicates. +4. The `Remind` command will then be executed and the `UI` will be updated with the filtered lists of persons and events. + +#### General design considerations + +- **Alternative 1 (Current choice): Updating list with predicate.** + - Pros: + - Reuses the logic of the original `find` command. + - The `UI` component is notified of the changes to the list and updates the UI accordingly. + - Cons: + - The `Model` component is tightly coupled with the `UI` component. +- **Alternative 2: Checking current list for birthdays and events, and adding to new list.** + - Pros: + - Easier to implement. + - Cons: + - Performance overhead. New addressbook objects needs to be created. + +[Scroll back to Table of Contents](#table-of-contents) + +
+ +## **Proposed Features** + ### \[Proposed\] Undo/redo feature #### Proposed Implementation @@ -172,11 +563,11 @@ Step 1. The user launches the application for the first time. The `VersionedAddr ![UndoRedoState0](images/UndoRedoState0.png) -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +Step 2. The user executes `delete_person 5` command to delete the 5th person in the address book. The `delete_person` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete_person 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. ![UndoRedoState1](images/UndoRedoState1.png) -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_person n/David …​` to add a new person. The `add_person` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. ![UndoRedoState2](images/UndoRedoState2.png) @@ -207,11 +598,11 @@ The `redo` command does the opposite — it calls `Model#redoAddressBook()`,
-Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +Step 5. The user then decides to execute the command `list_all`. Commands that do not modify the address book, such as `list_all`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. ![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_person n/David …​` command. This is the behavior that most modern desktop applications follow. ![UndoRedoState5](images/UndoRedoState5.png) @@ -229,15 +620,10 @@ The following activity diagram summarizes what happens when a user executes a ne * **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). + * Pros: Will use less memory (e.g. for `delete_person`, 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}_ - +[Scroll back to Table of Contents](#table-of-contents) -------------------------------------------------------------------------------------------------------------------- @@ -251,48 +637,130 @@ _{Explain here how the data archiving feature will be implemented}_ -------------------------------------------------------------------------------------------------------------------- +
+ ## **Appendix: Requirements** ### Product scope **Target user profile**: -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +This product is targeted at busy university students who struggle to manage +their interpersonal relationships and commitments due to the demands of their +academic and social lives. They are relatively tech savvy and prefer to use the +keyboard over the mouse, prefer short commands over full sentences. +These users seek an intuitive and efficient solution to help them stay organized, +prioritize their tasks and manage their time effectively. -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: +We provide students with an easy-to-use application to manage their social lives and +time better. Our app reminds students of their upcoming commitments and helps them to +prioritize their tasks. It also helps them to keep track of their friends’ birthdays and +other important events. ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | - -*{More to be added}* +| Priority | As a …​ | I want to …​ | So that I can…​ | +|----------|--------------------|----------------------------------------------------------------|--------------------------------------------------------------------------------| +| `* * *` | university student | see usage instructions | refer to instructions when I forget how to use the App | +| `* * *` | university student | add a new person | keep my address book up to date | +| `* * *` | university student | include optional fields when adding contacts | include comprehensive and personalized information for each contact | +| `* * *` | university student | assign contacts to groups | efficiently organise my contacts by grouping them together | +| `* * *` | university student | delete a person | remove contacts that I no longer need | +| `* * *` | university student | find a person by name | locate details of persons by name without having to go through the entire list | +| `* * *` | university student | find a person by group | locate details of persons based on which group the person is in | +| `* * *` | university student | find an event by event name | locate details of an event without having to go through the entire list | +| `* * *` | university student | find an event by people or groups attending | locate details of an event based on the people or groups attending | +| `* * *` | university student | find contacts and events at the same time | quickly search for contacts and events that are related to each other | +| `* * *` | university student | edit a person details | reflect any contact changes accordingly | +| `* * *` | university student | create an event | schedule and keep track of important commitments | +| `* * *` | university student | include optional fields when adding events | include comprehensive and personalized information for each event | +| `* * *` | university student | assign contacts to events | keep track of who is attending an event | +| `* * *` | university student | edit an event details | modify event details if the details of event has changed | +| `* * *` | university student | delete an event | remove events that are canceled or no longer relevant | +| `* * *` | university student | view all upcoming events on a separate event column in the GUI | simultaneously view contact details and event details | +| `* * *` | university student | be reminded on events and birthdays | so that i can remember upcoming social activities | + +[Scroll back to Table of Contents](#table-of-contents) + +
### 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, unless specified otherwise: +- the **System** is `FumbleLog` +- the **Person** is the `user` +- the **Actors** are `Computing student` + +**Use case: UC01 - Add a person** + +**MSS** +1. User requests to add persons +2. FumbleLog adds the person + + Use case ends. + +**Extensions** +* 1a. User supplies the wrong type of parameters when adding the person + + * 2a1. FumbleLog shows an error message. + + Use case resumes at step 1. +* 2a. Person is added with a group and that group is assigned to an event + + * 2a1. FumbleLog reloaded the event component and displays the newly added person in the event. + + Use case ends. -**Use case: Delete a person** +**Use case: UC02 - Edit a person** + +**MSS** +1. User requests to list persons +2. FumbleLog shows a list of persons +3. User request to edit a specific person in the list +4. FumbleLog edits the person with the new information + + Use case ends. + +**Extensions** +* 2a. List is empty + + Use case ends. + +* 3a. User supplies an invalid index to edit + + * 3a1. FumbleLog shows an error message. + + Use case resumes at step 2. + +* 3b. User modifies the name of the person + + * 3b1. FumbleLog updates the name of the person in all events that the person is assigned to. This includes persons in groups. + + Use case resumes at step 4. + +* 3c. User removes a group(s) from the person and that group(s) is assigned to an event. + + * 3c1. FumbleLog removes the person from the corresponding group in all events. + + Use case resumes at step 4. + +* 3d. User adds a group(s) to the person and that group(s) is assigned to an event. + * 3d1. FumbleLog adds the person to the corresponding group(s) in all events. + + Use case resumes at step 4. + +**Use case: UC03 - Delete a person** **MSS** 1. User requests to list persons -2. AddressBook shows a list of persons +2. FumbleLog shows a list of persons 3. User requests to delete a specific person in the list -4. AddressBook deletes the person +4. FumbleLog deletes the person Use case ends. @@ -304,17 +772,263 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli * 3a. The given index is invalid. - * 3a1. AddressBook shows an error message. + * 3a1. FumbleLog shows an error message. Use case resumes at step 2. -*{More to be added}* +* 3b. The person is assigned to an event. + + * 3b1. The person is removed from the event. + + Use case resumes at step 4. + +* 4a. The person is the last member of a group and that group is assigned to an event. + + * 4a1. The group is removed from the event. + + Use case ends. + +**Use case: UC04 - Add an event** + +**MSS** + +1. User requests to add a event +2. FumbleLog adds the event with the supplied information + + Use case ends. + +**Extensions** +* 1a. User supplies invalid parameters + + * 1a1. FumbleLog shows an error message + + Use case resumes at step 1. +* 1b. User supplies a date that is before the current date + + * 1b1. FumbleLog shows an error message + + Use case resumes at step 1. +* 1c. User supplies a start time that is after the end time + + * 1c1. FumbleLog shows an error message + + Use case resumes at step 1. +* 1d. User supplies a start time that is before the current time + + * 1d1. FumbleLog shows an error message + + Use case resumes at step 1. + + **Use case: UC05 - Edit an event** + + **MSS** +1. User request to edit a specific event in the list +2. FumbleLog edits the event with the new information + + Use case ends. + +**Extensions** +* 1a. List is empty + + Use case ends. + +* 1b. User supplies an invalid index to edit + + * 1b1. FumbleLog shows an error message. + + Use case resumes at step 1. +* 1c. User supplies an invalid parameter + * 1c1. FumbleLog shows an error message. + + Use case resumes at step 1. +* 1d. User supplies a date that is before the current date + + * 1d1. FumbleLog shows an error message + + Use case resumes at step 1. +* 1e. User supplies a start time that is after the end time + + * 1e1. FumbleLog shows an error message + + Use case resumes at step 1. +* 1f. User supplies a start time that is before the current time + + * 1f1. FumbleLog shows an error message + + Use case resumes at step 2. +* 2a. User enters a group and certain members of the group is already +assigned to the event individually. + + * 2a1. For each Event, duplicate members will be removed from the + individual Persons list. + + Use case ends + +**Use case: UC06 - Delete an event** + +**MSS** + +1. User requests to list events +2. FumbleLog shows a list of events +3. User requests to delete a specific event in the list +4. FumbleLog deletes the event + + Use case ends. + +**Extensions** + +* 2a. The list is empty + + Use case ends. + +* 3a. The given index is invalid + + * 3a1. FumbleLog shows an error message. + + Use case resumes at step 2. + +**Use case: UC07 - Assigning a person to an event** + +**MSS** +1. User requests to show a list of events +2. FumbleLog shows list of events +3. User requests to assign a person(s) to a specific event in the list +4. FumbleLog assigns the person(s) to the event +5. FumbleLog displays the all person(s) with that group under the event + + Use case ends. + +**Extensions** + +* 1a. The list is empty + + Use case ends. + +* 3a. User tries to assign a person to an invalid event + * 3a1. FumbleLog shows an error message + + Use case resumes at step 3. +* 3b. User tries to assign an invalid person to an event. + * 3b1. FumbleLog shows an error message. + + Use case ends. +* 4a. User tries to assign a person to an event that already has the person assigned + + Use case ends. + +**Use case: UC08 - Assigning a group to an event** + +**MSS** +1. User requests to show a list of events +2. FumbleLog shows list of events +3. User requests to assign a group(s) to a specific event in the list +4. FumbleLog assigns the group(s) to the event +5. FumbleLog displays the all person(s) with that group under the event + + Use case ends. + +**Extensions** + +* 1a. The list is empty + + Use case ends. + +* 3a. User tries to assign a group to an invalid event + * 3a1. FumbleLog shows an error message + + Use case resumes at step 3. + +* 3b. User tries to assign an invalid group to an event. + * 3b1. FumbleLog shows an error message. + + Use case ends. + +* 4a. User tries to assign a group to an event that already has the group assigned + + Use case ends. + +* 5a. A person is displayed as an individual and is a member to the group. + + * 5a1. FumbleLog removes the person from the individual list. + + Use case ends. + +**Use case: UC09 - Find persons** +1. User requests to list all the persons. +2. FumbleLog shows a list of persons +3. User requests to find persons by specifying a group or name +4. FumbleLog shows the list of persons that belong in the specified group + + Use case ends. + +**Extensions** + +* 3a. The group does not exist + + * 3a1. FumbleLog shows an empty list. + + Use case resumes at step 3. + +* 3b. The person does not exist + + * 3b1. FumbleLog shows an empty list. + + Use case resumes at step 3. + +* 4a. The list is empty + + Use case ends. + +**Use case: UC10 - Find events** +1. User requests to list all the events. +2. FumbleLog shows a list of events +3. User requests to find events by specifying a group or event name +4. FumbleLog shows the list of events that belong in the specified group + + Use case ends. + +**Extensions** + +* 3a. The group does not exist + * 3a1. FumbleLog shows an empty list. + + Use case resumes at step 3. +* 3b. The event does not exist + * 3b1. FumbleLog shows an empty list. +* 4a. The list is empty + + Use case ends. + + +**Use case: UC11 - Show reminders for events/birthdays happening soon** + +**MSS** +1. User requests a reminder for events/birthdays happening soon using the 'remind' command. +2. FumbleLog displays a list of events/birthdays happening in the next 7 days by default. + + Use case ends. + +**Extensions** + +* 1a. User can specify the number of days to look ahead for events and birthdays after the 'remind' command. + * 1a1. If the specified range is valid (e.g., a positive integer), FumbleLog shows the list of events/birthdays happening within the specified range of time (n days). + * 1a2. If the specified range is invalid (e.g., a negative integer), FumbleLog shows an error message. + + Use case ends. + +* 2a. The list is empty + + Use case ends. + +
### Non-Functional Requirements 1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. 2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. 3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +4. Should be able to hold up to 1000 events without a noticeable sluggishness in performance for typical usage. +5. The data stored on the hard drive should be light-weight and not take too much space. *{More to be added}* @@ -322,9 +1036,26 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli * **Mainstream OS**: Windows, Linux, Unix, OS-X * **Private contact detail**: A contact detail that is not meant to be shared with others +* **Person**: A person or entity that is associated with an event. A person contains a name and a list of contact details, such as phone number, email address, etc. +* **Event**: An encompassing term that refers to any organized occurrence or gathering, +which can include various types of activities, such as meetings, birthdays, and other scheduled events. +* **Meeting**: A specific type of event that involves the interaction of two or more individuals. Contacts or groups of contacts can be assigned to a single meeting. +* **Group**: A collection of contacts grouped together for organizational purposes. +Contacts or groups can be assigned to a single meeting, allowing for efficient management and coordination of events and interactions. +* **GUI**: Graphical User Interface +* **CLI**: Command Line Interface +* **UI**: User Interface +* **MSS**: Main Success Scenario +* **UC**: Use Case +* **VCS**: Version Control System +* **CI**: Continuous Integration + +[Scroll back to Table of Contents](#table-of-contents) -------------------------------------------------------------------------------------------------------------------- +
+ ## **Appendix: Instructions for manual testing** Given below are instructions to test the app manually. @@ -349,29 +1080,172 @@ testers are expected to do more *exploratory* testing. 1. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ +### Adding a person + +1. Adding a person when all persons are being shown + + 1. Test case: `add_person n/James p/93748274 e/james@gmail.com a/computing drive b/2001-10-20`
+ Expected: A contact is added to the list with the given parameters. Details of the added person will show in the response box. + +2. Adding a person when the contacts list is filtered + 2. Prerequisite: Filter the list using the `find_person` command or the `remind` command + + 3. Test case: `add_person n/John`
+ Expected: The contacts list shows all persons again. The contact is added to the contact list at the back. The added person's details is shown in the response box. + +3. Adding a person with a group that has been assigned to an event. + + 1. Prerequisite: The event list has an event with the group `CS2103T` assigned to it. The contact and event list is not filtered. + + 2. Test case: `add_person n/John g/CS2103T`
+ Expected: The person is added to the contact list. The added person's details are shown in the response box. The person is added to the event, under the group that it has been assigned to. + +### Editing a person + +1. Editing a person when all persons are being shown + + 1. Prerequisite: List all persons using the `list_all` command. There should be multiple persons in the contact list. + + 1. Test case: `edit_person 1 n/John Donuts`
+ Expected: The person's name should be updated. The person's details should be displayed in the response box. +2. Editing a person when the contacts list is filtered + + 1. Prerequisite: Filter the list using the `find_person` command or the `remind` command. Ensure that there is at least 1 person in the contact list. + + 1. Test case: `edit_person 1 n/Johnny`
+ Expected: The contact list stays filtered. If the person's name was the search term in `find_person`, the person should disappear. Used `list_all` to view the edited person. The details of the edited person should be displayed in the response box. + +3. Editing a person with a group that has already been assigned to an event + + 1. Prerequisite: The event list has an event with the group `CS2103T` assigned to it. The contact and event list is not filtered. + 1. Test case: `edit_person 1 n/Johnny g/CS2103T`
+ Expected: The person is edited in the contact list. The person's details are shown in the response box. The person is added to the event, under the group that it has been assigned to. + +4. Editing a person to unassign a group, given that the group has been assigned to an event + + 1. Prerequisite: The event list has an event with the group `CS2103T` assigned to it. The contact and event list is not filtered. + + 1. Test case: `edit_person 1 ug/CS2103T`
+ Expected: The person is edited in the contact list. The person's details are shown in the response box. The person is removed from the event, under the group that it has been assigned to. If the person is the last member of the group, the group is removed from the event. + ### 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. Prerequisites: List all persons using the `list_all` or `list_persons` command. Multiple persons in the list. - 1. Test case: `delete 1`
+ 1. Test case: `delete_person 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`
+ 1. Test case: `delete_person 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. +2. Deleting a person with a group that has been assigned to an event + + 1. Prerequisite: The event list has an event with the group `CS2103T` assigned to it. The contact and event list is not filtered. + + 1. Test case: `delete_person 1`
+ Expected: The person is deleted from the contact list. The person is removed from the event, under the group that it has been assigned to. If the person is the last member of the group, the group is removed from the event. + +### Finding a person + +1. Find a person while all persons are being shown + + 1. Prerequisite: There is a person with the name `Alex` in the contacts list + + 1. Test case: `find_person Alex`
+ Expected: All persons with the name `Alex` or the group `Alex` (not case sensitive) should be displayed in the newly filtered contacts list. The number of persons listed should be displayed in the response box. + +### Adding an event + +1. Adding an event while all events are being shown + + 1. Prerequisites: List all events using the `list_all` command. Multiple events should be shown. + + 1. Test case: `add_event m/FumbleLog presentation d/2023-11-30`
+ Expected: An event is added to the list with the given parameters. Details of the added event will show in the response box. +2. Adding an event while the events list is filtered + + 1. Prerequisite: Filter the list using the `find_event` command or the `remind` command + + 1. Test case: `add_event m/CS2103T lecture d/2030-11-30`
+ Expected: The event list shows all events again. The event is added to the event list at the back. The added event's details is shown in the response box. + +### Editing an event + +1. Editing an event while all events are being shown + + 1. Prerequisite: List all events using the `list_all` command. There should be multiple events in the event list. + + 1. Test case: `edit_event 1 m/CS2103T tutorial`
+ Expected: The entire event list will be shown again. The event's name should be updated to the given parameter. The event's details should be displayed in the response box. + +2. Editing an event while the events list is filtered + + 1. Prerequisite: Filter the list using the `find_event` command or the `remind` command. Ensure that there is at least 1 event in the event list. + + 1. Test case: `edit_event 1 m/CS2103T tutorial`
+ Expected: The event list stays filtered. If the event's name was the search term in `find_event`, the event should disappear. Use `list_all` to view the disappeared edited event. The details of the edited event should be displayed in the response box. + +3. Editing an event with a new group + + 1. Prerequisite: The event list has an event with the group `CS2103T` assigned to it. The contact and event list is not filtered. -1. _{ more test cases …​ }_ + 1. Test case: `edit_event 1 m/CS2103T tutorial g/CS2103T`
+ Expected: The event is edited in the event list. The event's details are shown in the response box. The group is added to the event, under which all persons part of the group is displayed. + +### Deleting an event + +1. Deleting an event while all events are being shown + + 1. Prerequisites: List all events using the `list_all` or `list_events` command. Multiple events in the list. + + 1. Test case: `delete_event 1`
+ Expected: First event is deleted from the list. Details of the deleted event shown in the status message. Timestamp in the status bar is updated. + + 1. Test case: `delete_event 0`
+ Expected: No event is deleted. Error details shown in the status message. Status bar remains the same. + +### Finding an event + +1. Find an event while all events are being shown + + 1. Prerequisite: There is an event with the name `CS2103T` in the events list + + 1. Test case: `find_event CS2103T`
+ Expected: All events with the name `CS2103T` or the group `CS2103T ` or assigned persons with name `CS2103T` (not case sensitive) should be displayed in the newly filtered events list. The number of events listed should be displayed in the response box. + +### Finding a person or event + +1. Find a person or event while all persons and events are being shown + + 1. Prerequisite: There is a person with the name `Alex` and an event with a person assigned named `Alex` in the contacts list and events list respectively + + 1. Test case: `find_all Alex`
+ Expected: All persons and events with the name `Alex` or the group `Alex` or for events, with assigned persons with name `Alex` (not case sensitive) should be displayed in the newly filtered contacts list and events list respectively. The number of persons and events listed should be displayed in the response box. + +### Remind + +1. Remind with default number of days, with all persons and events being shown + + 1. Prerequisite: All persons and events are being shown, using the `list_all` command. There should be multiple persons and events in the contact list and event list respectively. + + 1. Test case: `remind`
+ Expected: All events with dates that are 7 days away and persons with birthdays that are 7 days away will be displayed in the newly filtered contacts list and events list respectively. The number of days used to filter the lists will be displayed in the response box. If there are no persons with birthdays or events with dates that match the search term, then the persons and event list will be empty respectively. + +2. Remind with a set number of days + + 1. Prerequisite: All persons and events are being shown, using the `list_all` command. There should be multiple persons and events in the contact list and event list respectively. + + 1. Test case: `remind 10`
+ Expected: All events with dates that are 10 days away and persons with birthdays that are 10 days away will be displayed in the newly filtered contacts list and events list respectively. The number of days used to filter the lists will be displayed in the response box. If there are no persons with birthdays or events with dates that match the search term, then the persons and event list will be empty respectively. ### Saving data 1. Dealing with missing/corrupted data files - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. FumbleLog will load with empty contacts list and events list in this event. + -1. _{ more test cases …​ }_ +[Scroll back to Table of Contents](#table-of-contents) diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 275445bd551..a9d7886aee8 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 [FumbleLog’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 57437026c7b..202f5d1b27b 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,195 +3,838 @@ 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. +FumbleLog is a **productivity desktop application** built to for **NUS Computing students** to help you manage contacts and track events. +It is designed to be an easy-to-use, one-stop platform for all your scheduling needs. +In this user guide, you will learn the basics of our application and how you can use it to manage your tasks and interpersonal relationships. + +
+ +# Table of Contents * Table of Contents {:toc} -------------------------------------------------------------------------------------------------------------------- -## Quick start +
+ +# Who is this guide for? +Our guide is made for FumbleLog users of all experiences! Refer to the table below to find out which section of the guide is most relevant to you. + +| **If you are...** | **You should...** | +|:-------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| +| New to FumbleLog | Read the [Quick Start](#quick-start) section to get started. After setting up, you can go through a step-by-step [Tutorial](#fumblelog-tutorial) of our application. | +| An experienced user | Skip to the [Commands Summary](#command-summary) section for a quick overview of all the commands, or have a look at our [Features](#features) for a detailed look at each of our features | + +
-1. Ensure you have Java `11` or above installed in your Computer. +# Quick start -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +**1. Ensure you have the right environment.** +- Before you begin, make sure you have `Java 11` or above installed in your computer. + - To check if you have java installed or your installed java version: + - Open a command terminal (Command Prompt or Terminal, depending on your operating system) and use the command: `java --version`. + - You should see the java version if you have java installed. + - If you do not have java installed, you can download it from [here](https://www.oracle.com/sg/java/technologies/javase/jdk11-archive-downloads.html). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +**2. Download our FumbleLog Application.** +- Visit the official FumbleLog release page [here](https://github.com/AY2324S1-CS2103T-T12-2/tp/releases). +- Download the latest version of `fumblelog.jar` from the release page. -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +**3. Set up your home folder.** +- Choose a folder on your computer where you want to store you FumbleLog application, or create a new folder. +- Copy the `fumblelog.jar` file into the folder you have chosen or created. -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.
+
+ +**4. Launch the application.** +- In your command terminal, use the `cd` command to navigate to the folder where you have placed the `fumblelog.jar` file. +- Run the application using the command: `java -jar fumblelog.jar`. You should now be able to see the FumbleLog user interface! + - The application contains sample data for you to play around with. + - Some blocks may appear red, indicating expired events, so do not be alarmed. + +![Ui](images/Ui.png) + +**5. Try out some simple commands!** +- Type commands into the command box and press `Enter` to execute it. e.g. typing **`help`** and pressing `Enter` will open the help window.
Some example commands you can try: - * `list` : Lists all contacts. + * `list_persons` : Lists all persons stored in FumbleLog. + * `add_person n/John Doe` : Adds a person named `John Doe` to the FumbleLog persons list. + * `delete_person 3` : Deletes the 3rd person shown in the current persons list. + * `exit` : Exits FumbleLog application. + +
- * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. +**6. Learn more about FumbleLog** +- Refer to [Orientation to the Graphical User Interface](#orientation-to-the-graphical-user-interface-gui) below for an orientation on FumbleLog. +- Refer to [FumbleLog Tutorial](#fumblelog-tutorial) for a more extensive guide on how to use FumbleLog. +- If you think you're ready to learn more advanced commands, refer to [Features](#features) below for more details on FumbleLog's commands. - * `delete 3` : Deletes the 3rd contact shown in the current list. - * `clear` : Deletes all contacts. +[Scroll back to Table of Contents](#table-of-contents) - * `exit` : Exits the app. +-------------------------------------------------------------------------------------------------------------------- + +
+ +# Orientation to the Graphical User-Interface (GUI) +![User Interface](images/userInterfaceTutorial.png) + +Refer to the table below for details on each GUI component + +| **GUI Component** | **Description** | +|:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------| +| Menu Bar | Contains the `File` dropdown menu which allows you to exit the application and the `Help` dropdown menu which allows you to access the user guide | +| Command box | Type your commands here and press `Enter` to execute them. | +| Response box | The response to your commands will be shown here. If your command is invalid, the correct command format will be shown instead. | +| Contact list | Displays the list of persons in FumbleLog. You can scroll through the list of persons using the scroll bar on the right of the list. | +| Event list | Displays the list of events in FumbleLog. You can scroll through the list of events using the scroll bar on the right of the list. | +| Index | Displays the index of the person or event in their respective lists. This index is used in certain commands. i.e. editing persons or events. | + +
+ + **Note about red coloured events:**
+An expired event will appear red in the event list. This is to highlight to you that the event has passed. + +This is illustrated as follows: +![Expired Event](images/ExpiredEvent.png) + +
+ +[Scroll back to Table of Contents](#table-of-contents) + + +-------------------------------------------------------------------------------------------------------------------- + +
+ +# FumbleLog Tutorial +This tutorial is designed for new users looking to get started using FumbleLog. In this tutorial, you will find step-by-step instructions on how to use commands in FumbleLog to help you manage +your contacts and events. + +1. First launch FumbleLog. You may refer to the [Quick Start](#quick-start) guide if you have forgotten how to. +2. Let's first try **adding a person**, `Mary Lee`, to your contact list. Enter the command: `add_person n/Mary Lee p/91234567 e/mary@gmail.com a/Mary Street #01-01 b/2001-12-12 g/Family`. You should see FumbleLog successfully adding the contact to the contact list: +![Tutorial Add](images/tutorialAdd.png) +3. Now, lets try **editing the name and email** of your contact. Let's use the index of `Mary Lee` shown in the list (in this case 1), and edit her information: `edit_person 1 n/John Doe e/John@gmail.com`. FumbleLog should reflect the changes to your contact immediately: +![Tutorial Edit](images/tutorialEdit.png) +4. Try adding a few more contacts and assign them to the same `Family` group using the `g/` parameter. Your contact list should look something like this: +![Tutorial Add More](images/tutorialAddMore.png) +5. Now, lets say `John`'s birthday is in a few weeks. We can **add this event** to FumbleLog using this command: `add_event m/John birthday d/2023-12-12`. +![Tutorial Event Add](images/tutorialEventAdd.png) +6. If everyone in the `Family` group is attending `John`'s birthday, you can easily assign every contact in the group to the event. In this case, simply **edit the event** by assigning the `Family` group, as such: `edit_event 1 g/Family`. Now you should see everyone in the `Family` group is assigned to `John`'s birthday. +![Tutorial Event Edit](images/tutorialEventEdit.png) +7. Finally, once the event is over, you can **delete the event** by using the index of the event (in this case 1): `delete_event 1`. +8. **Well done! 👍** You have mastered the basics of FumbleLog! You can now visit the [Features](#features) section to learn advanced commands! + +[Scroll back to Table of Contents](#table-of-contents) -1. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- +
-## Features +# Features
**:information_source: Notes about the command format:**
-* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. +* Words in `UPPER_CASE` are parameters to be supplied with the command.
+ e.g. in `add_person n/NAME`, `NAME` is a parameter which can be used as `add_person n/John Doe`. * Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. + e.g `n/NAME [g/GROUP]` can be used as `n/John Doe g/family` or as `n/John Doe`. -* Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. +* Items with `…`​ after them can be used multiple times (or not at all).
+ e.g. `n/NAME [g/GROUP]…​` can be used as `n/John Doe` (i.e. 0 times), `n/John Doe g/friend`, `n/John Doe g/friend g/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. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
+* Invalid prefixes, such as `t/` will be regarded as a part of the input, for example `n/John Doe t/friend`, the name + will be parsed as `John Doe t/friend` instead of `John Doe`. + +* Extraneous parameters for commands that do not take in parameters (such as `help`, `list_all`, `exit` and `clear`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. +* If any of the commands provided are invalid/do not follow the necessary format, an error message will be displayed. + * If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application.
+
+ ### Viewing help : `help` -Shows a message explaning how to access the help page. +Shows a pop-up window with a link to the user guide for help. -![help message](images/helpMessage.png) +- No response in the response box should be expected after clicking the help button. Format: `help` +![Helptab](images/Helptab.png) + +[Scroll back to Table of Contents](#table-of-contents) + +
+ +## Commands for Persons + +### Adding a person: `add_person` + +FumbleLog allows you to add personalised contacts to your contact list. +Format: `add_person n/NAME [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [b/BIRTHDAY] [r/REMARK] [g/GROUP]…​` -### Adding a person: `add` +**Acceptable values for each parameter:** -Adds a person to the address book. +| Parameter | Format | Example | +|----------------|-------------------------------------------------------------------------------------------------------|----------------------------------| +| `NAME` | Use `a-z`, `A-Z`, `0-9` and whitespaces only. A person's name cannot contain **only** numbers. | `John Doe` | +| `PHONE_NUMBER` | Use `0-9` only and should be at least 3 digits long and maximum of 17 digits **without** whitespaces. | `p/98765432` | +| `EMAIL` | Be in format `local-part@domain`. Refer to the [FAQ](#faq) section for more details. | `johndoe@gmail.com` | +| `ADDRESS` | Use any characters including whitespaces. | `John Street, block 123, #01-01` | +| `BIRTHDAY` | Should be in format `yyyy-MM-dd` and should not be later than current date. | `2001-12-30 ` | +| `REMARK` | Use any characters including whitespaces. | `Owes me $2.` | +| `GROUP` | Use `a-z`, `A-Z`, `0-9` only and **must not** contain any whitespaces. | `CS2103T` | -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +
+ +**Below are some examples on how to use the `add_person` command:** + +- `add_person n/Jonathan`: Adds a person with name `Jonathan`. +- `add_person n/Betsy Crowe e/betsycrowe@example.com a/Computing Drive p/12345678`: Adds a person with name `Betsy Crowe`, with email `betsycrowe@example.com`, with address `Computing Drive` and phone `12345678`. + +
+ +**:information_source: Notes on the `add_person` command:** + +* You must include the person's name when adding a contact, but the other fields are optional. +* A person can be assigned to 0 or more groups. +* Persons with the exact same name as another person cannot be added. +* When a person with an assigned group and that group has been assigned to an [event](#commands-for-events), that person will be displayed with the respective event. + +
+ +**Expected output when the command succeeds:** +* Input: `add_person n/James p/93748274 e/james@gmail.com a/computing drive b/2001-10-20` + +![Addperson](images/Addperson.png) + + +[Scroll back to Table of Contents](#table-of-contents) + +
+ +### Editing a person : `edit_person` + +FumbleLog allows you to edit your contact list so that it is always up to date. + +Format: `edit_person PERSON_INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [b/BIRTHDAY] [r/REMARK] [g/GROUP]…​ [ug/GROUP]…​` + +**Acceptable values for each parameter:** + +| Parameter | Format | Example | +|---------------|------------------------------------------------------------------------------------------------------------------|----------------------------------| +| `PERSON_INDEX`| An index in the current displayed contacts list in FumbleLog | `1` | +| `NAME` | Use `a-z`, `A-Z`, `0-9` and whitespaces only. A person's name cannot be empty and must contain **only** numbers. | `John Doe` | +| `PHONE_NUMBER`| Use `0-9` only and should be at least 3 digits long and maximum of 17 digits **without** whitespaces. | `p/98765432` | +| `EMAIL` | Be in format `local-part@domain`. Refer to the [FAQ](#faq) section for more details. | `johndoe@gmail.com` | +| `ADDRESS` | Use any characters including whitespaces. | `John Street, block 123, #01-01` | +| `BIRTHDAY` | Have format `yyyy-MM-dd` and should not be later than current date. | `2001-12-30 ` | +| `REMARK` | Use any characters including whitespaces. | `Owes me $2.` | +| `GROUP` | Use `a-z`, `A-Z`, `0-9` only and must not contain any whitespaces. | `CS2103T` | + +
+ +**Below are some examples on how to use the `edit_person` command:** + +* `edit_person 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_person 3 n/Betsy Crower b/2023-09-29`: Edits the name of the 3rd person to be `Betsy Crower` and changes the birthday to 29th Sep 2023. Any events that Betsy Crower is assigned to is also updated with this new name. + +
+ +**:information_source: Notes on the `edit_person`command:**
+* At least one of the parameters must be provided. +* Existing values will be updated to the input values for all values except for `GROUP` + * Parameters `p/`, `e/`, `a/` and `b/` can be empty strings. Doing so will clear the current values for the respective fields. i.e. `edit_person 1 a/` will remove the current `ADDRESS`. + * Parameter `g/` is used to **assign a person** to a group. If the person is already assigned to the group, the group will not be added again. + * Parameter `ug/` is used to **unassign a person** from a group. Once unassigned, the person's name will not be displayed in events that the group is assigned to. +* When you edit a person's name, the person's name will be updated in all [events](#commands-for-events) that the person is assigned to. Same for groups if the person's group is assigned to events. +* There will not be an error shown in the case that you edit a person's values to be the same as it currently is. i.e. if the first person's name on the person list is Alex, `edit_person 1 n/Alex` is a valid command and will not show an error message. -
:bulb: **Tip:** -A person can have any number of tags (including 0)
-Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` -### Listing all persons : `list` +**Expected output when the command succeeds:** +* Input: `edit_person 1 n/Alexa Yeoh` changes the name of the 1st person to be `Alexa Yeoh`, leaving the rest of the fields unchanged. + ![Editperson](images/Editperson.png) -Shows a list of all persons in the address book. +
-Format: `list` -### Editing a person : `edit` +
-Edits an existing person in the address book. +**:exclamation: Disclaimer: Editing a person with a filtered contacts list might cause the person to disappear. Do not worry, your data is not deleted.**
-Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +- Lets say you used the command `find_person Alex`, to show all the persons with `Alex` in their name. See: [find_person](#locating-persons-by-name-or-group-findperson) +- The person list is filtered to show all the persons with `Alex` in their name. +- You then edit the person `Alex`'s name to `Bob`. +- `Alex` will disappear from the person list, because your previous search term `Alex` no longer matches the new name of the person, `Bob`. +- To see `Bob` in the person list again, you can use the [`list_persons`](#listing-all-persons--listpersons) command to bring back the whole list of persons. +- In contrast with the above scenario, using an [`add_person`](#adding-a-person-addperson) command will automatically bring back the whole list of persons, to show you that your new person has been added to FumbleLog. + +
-* 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. -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. +[Scroll back to Table of Contents](#table-of-contents) -### Locating persons by name: `find` +
-Finds persons whose names contain any of the given keywords. +### Deleting a person : `delete_person` -Format: `find KEYWORD [MORE_KEYWORDS]` +FumbleLog allows you to organize your contact list by deleting contacts that are no longer relevant. + +Format: `delete_person PERSON_INDEX` + +**Acceptable values for each parameter:** + +| Parameter | Format | Example | +|----------------|----------------------------------------------------|---------| +| `PERSON_INDEX` | An index in the currently displayed contacts list. | `1` | + + +**Below are some examples on how to use `delete_person` command:** + +* `list_all` followed by `delete_person 2`: Deletes the 2nd person in the person list. +* `find_all Betsy` followed by `delete_person 1`: Deletes the 1st person in the filtered list as a result of the `find` command. i.e Any person named `Betsy` at index `1` will be deleted. + +
-* The search is case-insensitive. e.g `hans` will match `Hans` +**:information_source: Notes on `delete_person` command:**
+* The index refers to the index number shown in the displayed person list. +* When a person is deleted, any [events](#commands-for-events) that the person is assigned to will also be updated, i.e. the person will be unassigned from the event. + +
+ +**Expected output when the command succeeds:** + +Input: `delete_person 1` deletes the first person on the list. + +![DeletePerson](images/DeletePerson.png) + +[Scroll back to Table of Contents](#table-of-contents) +
+ +### Locating persons by name or group: `find_person` + +If you would like to quickly search for a contact, FumbleLog allows you to search for contacts by name or group. + +Format: `find_person KEYWORD [MORE_KEYWORDS]` + +**Acceptable values for each parameter:** + +| Parameter | Format | Example | +|------------------------------|----------------------------------------------------------------------------|----------------------| +| `KEYWORD` or `MORE_KEYWORDS` | Use any characters including whitespace. Must not only contain whitespaces | `Alice` or `friends` | + +**Below are some examples on how to use `find_person` command:** + +* `find_person John`: Displays `john` and `John Doe` +* `find_person friends`: Displays `Alex Yeoh` as he belongs to the `friends` group. + +
+ +**:information_source: Notes on `find_person` command:**
+* Only **full words** will be matched e.g. `Han` will not match `Hans` +* The search is **case-insensitive**. e.g `hans` will match `Hans` * The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. +* Persons matching at least one keyword will be returned. + e.g. `find_person Hans Bo` will return `Hans Gruber` and `Bo Yang`. + +
+ +
+ +**Expected output when the command succeeds:** + +Input: `find_person Alexa` displays all contacts with the name `Alexa` in the contact list. + +![result for 'find_person alex david'](images/findFriendsResult.png) + + +
+ +**:exclamation: Disclaimer when using the `find_person` command:**
+ +* FumbleLog will return an **empty person list** when there are no keyword matches. **Your data will not be deleted.** + +
+ +[Scroll back to Table of Contents](#table-of-contents) + +
+ +### Listing all persons : `list_persons` + +FumbleLog restores any filtered contacts list using `list_persons`. + +Format: `list_persons` + +**Below are some examples on how to use `list_persons` command:**
+* `list_persons`: Lists all your entire contacts list in FumbleLog. + +
+ +**:information_source: Notes on `list_persons` command:**
+* Any text after `list_persons` command will be ignored and the command will be executed as normal. + +
+ +[Scroll back to Table of Contents](#table-of-contents) + +
+ +## Commands for Events + +### Adding an event : `add_event` + +Apart from allowing you to add contacts, FumbleLog allows you to keep track of your daily commitments by +allowing you to add events. You can also choose to assign existing contacts or groups to each event. + +Format: `add_event m/EVENT_NAME d/DATE [s/START_TIME] [e/END_TIME] [n/PERSON_NAME]... [g/GROUP]...` + +**Acceptable values for each parameter:** + +| Parameter | Format | Example | +|-----------------------------|-------------------------------------------------------------------------------------------|-------------------| +| `EVENT_NAME` | Use `a-z`, `A-Z`, `0-9` and whitespaces only. | `CS2103T meeting` | +| `DATE` | Have format `yyyy-MM-dd` and should not be earlier than current date. | `2023-12-01` | +| `START_TIME` and `END_TIME` | Have format `HHmm`. `START_TIME` should be earlier than `END_TIME`. | `1400` | +| `PERSON_NAME` | Multiple persons can be assigned to an event but only existing persons name can be added. | `John Doe` | +| `GROUP` | Multiple groups can be assigned to an event but only existing groups can be added. | `CS2103T` | + +
+ +**Below are some examples on how to use `add_event` command:** + +* `add_event m/FumbleLog presentation d/2023-10-30`: Adds an event with name `FumbleLog presentation` and with date `2023-10-30`. +* `add_event m/CS2101 OP2 d/2023-10-05 s/1500 e/1700 n/Ken g/CS2103T g/CS2101`: Adds an event with name `CS2101 OP2`, with date `2023-10-05`, with start time `1500`, with end time `1700`, assigns contact with name `Ken` and groups `CS2103T`, `CS2101` to the event. + + +
+ +**:information_source: Notes on `add_event` command:**
+- `START_TIME` and `END_TIME` are optional. +- `PERSON_NAME` and `GROUP` is optional. +- Multiple persons and groups can be added at once, however only existing groups and persons can be added. +- The provided values for `DATE`, `START_TIME` and `END_TIME` must represent future date and time; past values are not allowed. +- The given `START_TIME` must be before the given `END_TIME`. +- If the meeting is added successfully, it will automatically be sorted by date and time with the earliest meeting at the top of the list. +- Note that if a person appears under multiple groups, e.g `Alvin` is in groups `classmates` and `friends`, the name `Alvin` will appear under both groups when displayed in the events list. This is an intended behavior for you to see everyone in the groups that are assigned to the event.
+This is illustrated as follows: + ![Person appearing multiple times](images/DuplicatePersonInDifferentGroups.png) + +
+ + +**This should be the expected output when the command succeeds:** +Input: `add_event m/CS2103T meeting d/2023-10-27 s/1400 e/1600` + +![EventAdd](images/Eventadd.png) + + +[Scroll back to Table of Contents](#table-of-contents) + +
+ +### Editing an event : `edit_event` + +If the details of an event has changed or if you have made a mistake when adding an event, FumbleLog allows you to easily edit your event details with the latest updated information. You can use this command +to assign more contacts or groups to the event using the `n/` or `g/` parameter respectively, or unassign contacts or groups using `u/` or `ug/` respectively. + +Format: `edit_event EVENT_INDEX [m/EVENT_NAME] [d/DATE] [s/START_TIME] [e/END_TIME] [n/PERSON_NAME]... [u/PERSON_NAME]... [g/GROUP]... [ug/GROUP]...` + +**Acceptable values for each parameter:** + +| Parameter | Format | Example | +|-----------------------------|-------------------------------------------------------------------------------------------|-------------------| +| `EVENT_INDEX` | An index in the currently displayed events list. | `1` | +| `EVENT_NAME` | Use `a-z`, `A-Z`, `0-9` and whitespaces only. | `CS2103T meeting` | +| `DATE` | Have format `yyyy-MM-dd` and should not be earlier than current date. | `2023-12-01` | +| `START_TIME` and `END_TIME` | Have format `HHmm`. `START_TIME` should be earlier than `END_TIME`. | `1400` | +| `PERSON_NAME` | Multiple persons can be assigned to an event but only existing persons name can be added. | `John Doe` | +| `GROUP` | Multiple groups can be assigned to an event but only existing groups can be added. | `CS2103T` | + +
+ +**Below are some examples on how to use `edit_event` command:** + +* `edit_event 1 m/FumbleLog meeting`: Edits the name of event at index 1 to `FumbleLog meeting`. +* `edit_event 1 s/1500 e/1700`: Edits the start and end time to `1500` and `1700` respectively. If the event initially does not have a start and end time, the respective times will be added to the event. +* `edit_event 1 u/Ken`: Unassigns the person `Ken` from the event. + +
+ +**:information_source: Notes on `edit_event` command:**
+* At least one of the optional parameters required. +* Existing values will be updated to the input values, except for `PERSON` AND `GROUP`. +* Only parameters `s/` and `e/` can be empty strings. Doing so will remove the current values. i.e. `edit_event 1 s/` will remove the current `START_TIME`. +* `PERSON` and `GROUP` edits are cumulative and will add to the current list of persons and groups. +* The given `DATE`, `START_TIME` and `END_TIME` cannot be a time in the past. +* Note that if a person appears under multiple groups, e.g `Alvin` is in groups `classmates` and `friends`, the name `Alvin` will appear under both groups when displayed in the events list. This is an intended behavior for you to see everyone in the groups that are assigned to the event.
+This is illustrated as follows: +![Person appearing multiple times](images/DuplicatePersonInDifferentGroups.png) +
+ + +**This should be the expected output when the command succeeds:** +Input: `edit_event 1 m/tP week 3 meeting d/2023-10-30` + +![Eventedit](images/Eventedit.png) + + +
+ + +
+ +**:exclamation: Disclaimer: Editing a person with a filtered contacts list might cause the person to disappear. Do not worry, your data is not deleted.**
+ +- Let say you have an event named `TP meeting` stored in FumbleLog and you used `find_event meeting`. See: [find_event](#locating-events-by-name-group-or-person-findevent) +- The event list will be filtered to show all the persons with `meeting` in their name. +- You then edit `TP meeting` event's name to `TP sprint`. +- `TP meeting` disappears from the person list, because your previous search term `meeting` no longer matches the new event name, `TP sprint`. +- To see `TP sprint` in the event list again, you can use the [list_events](#listing-all-events-listevents) command to bring back the whole list of events. +- In contrast with the above scenario, using an [add_event](#adding-an-event--addevent) command will automatically bring back the whole list of events, to show you that your new event has been added to FumbleLog. +
+ +[Scroll back to Table of Contents](#table-of-contents) + +
+ +### Deleting an event : `delete_event` + +FumbleLog helps you to organise your event list better by allowing you to delete events that are in the past or no longer relevant. + +Format: `delete_event EVENT_INDEX` + +**Acceptable values for each parameter:** + +| Parameter | Format | Example | +|---------------|--------------------------------------------------|----------------------------------| +| `EVENT_INDEX` | An index in the currently displayed events list. | `1` | + +**Below are some examples on how to use `delete_event` command:** +* `delete_event 1`: Deletes the 1st event in the event list. +* `find_all meeting` followed by `delete_event 1`: Deletes the 1st event in the results of the `find` command. + +
+ +**:information_source: Notes on `delete_event` command:**
+- The index refers to the index number shown in the displayed person list. + +
+ +**This should be the expected output when the command succeeds:** +* Input: `delete_event 1` + +![EventDelete](images/Eventdelete.png) + +[Scroll back to Table of Contents](#table-of-contents) + +
+ +### Locating events by name, group or person: `find_event` + +FumbleLog also allows you to quickly search for events whose name or groups contain any of the given keywords. + +Format: `find_event KEYWORD [MORE_KEYWORDS]` + +**Acceptable values for each parameter:** + +| Parameter | Format | Example | +|------------------------------|----------------------------------------------------------------------------|---------------------| +| `KEYWORD` or `MORE_KEYWORDS` | Use any characters including whitespace. Must not only contain whitespaces | `Alice` or `Friends` | + +**Below are some examples on how to use `find_event` command:** +* `find_event meeting`: Returns `meeting` and `CS2103T meeting` +* `find_event friends` returns `meeting` if it contains the `friends` group. + + +
+ +**:information_source: Notes on `find_event` command:**
+ +* `find_event` searches the name of the `Event`, `Group` and `Person` that they are assigned + to and will display them accordingly. +* Only full words will be matched e.g. `meeting` will not match `meetings` +* FumbleLog will display an empty event list when there are no keyword matches. +* The keywords are **not** case-sensitive. e.g `meeting` will match `Meeting` +* Events matching at least one keyword will be returned (i.e. `OR` search). + e.g. `Meetings TP` will return `Meetings`, `TP deadline` + +
+ +
+ +**This should be the expected output when the command succeeds:**
+Input: `find_event meeting family` + +![EventFind](images/Eventfind.png) + +[Scroll back to Table of Contents](#table-of-contents) + +
+ +### Listing all events: `list_events` + +After using FumbleLog `find_event` or `find_all` command which filters the event list, you can use `list_events` to display the full event list again. + +Format: `list_events` + +**Below is an example on how to use `list_persons` command:** +* `list_persons`: Lists all your saved contacts in FumbleLog. + + +
+ +**:information_source: Notes on `list_events` command:**
+- Events are sorted by date and time, with the earliest event at the top of the list. +- Any text after the `list_events` command will be ignored and the command will be executed as normal. + +
+ +[Scroll back to Table of Contents](#table-of-contents) + +
+ +## General commands + +### Show all upcoming events and birthdays : `remind` + +If you would like to have a quick overview of your upcoming commitments, you can use the `remind` command and FumbleLog will display all upcoming +events and birthdays. + +Format: `remind [NUM_OF_DAYS]` + +**Acceptable values for each parameter:** + +| Parameter | Format | Example | +|---------------|------------------------------------------------------|----------------------------------| +| `NUM_OF_DAYS` | A positive integer with maximum value of 999999999. | `1` | + + +**Below are some examples on how to use `remind` command:** +* `remind`: Shows all events and birthdays happening in the next 7 days. +* `remind 3`: Shows all events and birthdays happening in the next 3 days. + + +
+ +**:information_source: Notes on `remind` command:**
+* `NUM_OF_DAYS` is optional. It specifies the number of days you would like to look ahead for events and birthdays. +* If `NUM_OF_DAYS` is not specified, the default value is 7 days. + +
+ +
+ +**Expected output when the command succeeds:**
+Input: `remind` + +![Remind](images/Remind.png) + +[Scroll back to Table of Contents](#table-of-contents) + +
+ +### Finding persons and events: `find_all` + +If you would like to search for your contacts and events at the same time, you can use the `find_all` command to find all +persons and events whose names or groups contain any of the given keywords. + +Format: `find_all KEYWORD [MORE_KEYWORDS]` + +**Acceptable values for each parameter:** + +| Parameter | Format | Example | +|------------------------------|----------------------------------------------------------------------------|---------------------| +| `KEYWORD` or `MORE_KEYWORDS` | Use any characters including whitespace. Must not only contain whitespaces | `Alice` or `Friends` | + +**Below are some examples on how to use `find_event` command:** +* `find_all John`: Returns `john` and `John Doe` in the persons list and `John's birthday` in the events list. +* `find_all friends`: Returns `Alex Yeoh` as he belongs to the `friends` group in the persons list, + and `CS2103T meeting` as it contains the `friends` group in the events list. + +
+ +**:information_source: Notes on `find_all` command:**
* 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). +* FumbleLog will return an empty person/event list when there are no keyword matches. +* 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` +* Persons and events matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` -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` +**Expected output when the command succeeds:**
+Input: `find_all friends` -Deletes the specified person from the address book. +![Findall](images/Findall.png) -Format: `delete INDEX` +[Scroll back to Table of Contents](#table-of-contents) -* 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, …​ +
+ +### Listing all persons and events: `list_all` + +If you would like to list all your contacts and events, you can use the `list_all` command to display the full list of contacts and events. + +Format: `list_all` + +**Below is an example on how to use `list_all` command:** +* `list_all`: Lists all your contacts and events in FumbleLog. + + +
-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. +**:information_source: Notes on `list_all` command:**
+- Events are sorted by date and time, with the earliest event at the top of the list. +- Any text after the `list_all` command will be ignored and the command will be executed as normal. + +
+ + +[Scroll back to Table of Contents](#table-of-contents) + +
### Clearing all entries : `clear` -Clears all entries from the address book. +Clears all contacts and events from the FumbleLog. Format: `clear` +**Below is an example on how to use `clear` command:** +* `clear`: Clears all your contacts and events data in FumbleLog. + + +
+ +**:information_source: Notes on `clear` command:**
+- Events are sorted by date and time, with the earliest event at the top of the list. +- Any text after the `clear` command will be ignored and the command will be executed as normal. + +
+ +
+:exclamation: **Warning: This command is irreversible and data cannot be recovered once cleared! Be very sure you would like to erase all of your data before executing this command!**.
+ +
+ ### Exiting the program : `exit` Exits the program. Format: `exit` +[Scroll back to Table of Contents](#table-of-contents) + +-------------------------------------------------------------------------------------------------------------------- + +
+ +# How we manage your data + ### Saving the data -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +FumbleLog data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. ### Editing the data file -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +FumbleLog data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file.
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it. +If your changes to the data file makes its format invalid, FumbleLog will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
-### Archiving data files `[coming in v2.0]` - -_Details coming soon ..._ +[Scroll back to Table of Contents](#table-of-contents) -------------------------------------------------------------------------------------------------------------------- -## FAQ +
+ +# FAQ **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: + Step 1: Install the app in the other computer + Step 2: Go to your previous computer and obtain your old data file. You can find it at `[JAR file location]/data/addressbook.json`. + Step 3: Copy the file from step 2 and move it to the data folder in your new computer, by replacing the new empty data file. + Your data is now all restored! +**Q**: What are the constraints for email addresses?
+**A**: Emails should be of the format `local-part@domain` and adhere to the following constraints: + 1. The local-part should only contain alphanumeric characters and these special characters (like '+' and '_'). +The local-part may not start or end with any special characters. + 2. This is followed by a '@' and then a 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 + + have each domain label consist of alphanumeric characters, separated only by hyphens, if any. + +[Scroll back to Table of Contents](#table-of-contents) -------------------------------------------------------------------------------------------------------------------- -## Known issues +
+ +# Known issues 1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. +[Scroll 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` +
+ +# Command summary + +### Commands for Persons + +| Action | Format | Examples | +|-------------------|---------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------| +| **Add Person** | `add_person n/NAME [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [b/BIRTHDAY] [r/REMARK] [g/GROUP]…​` | `add_person n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 g/friend g/colleague` | +| **Edit Person** | `edit_person PERSON_INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [r/REMARK] [g/GROUP]…​` | `edit_person 2 n/James Lee e/jameslee@example.com` | +| **Delete Person** | `delete_person PERSON_INDEX` | `delete_person 3` | +| **Find Person** | `find_person KEYWORD [MORE_KEYWORDS]` | `find_person James Jake` | +| **List Persons** | `list_persons` | | + +
+ +### Commands for Events + +| Action | Format | Examples | +|------------------|-------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------| +| **Add Event** | `add_event m/EVENT_NAME d/DATE [s/START_TIME] [e/END_TIME] [n/PERSON_NAME]…​ [g/GROUP]…​` | `add_event m/FumbleLog meeting d/2023-10-05 s/1500 e/1700 n/Ken g/CS2103T g/CS2101` | +| **Edit Event** | `edit_event EVENT_INDEX [m/EVENT_NAME] [d/DATE] [s/START_TIME] [e/END_TIME] [n/PERSON_NAME]…​ [u/PERSON_NAME]…​ [g/GROUP]…​ [ug/GROUP]…​` | `edit_event 1 m/tP week 3 meeting d/2023-10-05 s/1500 e/1700 n/Ken g/CS2103T g/CS2101` | +| **Delete Event** | `delete_event EVENT_INDEX` | `delete_event 1` | +| **Find Event** | `find_event KEYWORD [MORE_KEYWORDS]` | `find_event meeting` | +| **List Events** | `list_events` | | + +
+ +### General commands + +| Action | Format | Examples | +|--------------|------------------------------------|------------------------| +| **Remind** | `remind [NUM_OF_DAYS]` | `remind` or `remind 4` | +| **List All** | `list_all` | | +| **Find All** | `find_all KEYWORD [MORE_KEYWORDS]` | `find_all John` | +| **Clear** | `clear` | | +| **Exit** | `exit` | | +| **Help** | `help` | | + +[Scroll back to Table of Contents](#table-of-contents) diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..db08903e8c6 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: FumbleLog theme: minima header_pages: @@ -8,8 +8,9 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2324S1-CS2103T-T12-2/tp" github_icon: "images/github-icon.png" plugins: - jemoji + - jekyll-seo-tag diff --git a/docs/_includes/custom-head.html b/docs/_includes/custom-head.html index 8559a67ffad..693800f48ac 100644 --- a/docs/_includes/custom-head.html +++ b/docs/_includes/custom-head.html @@ -3,4 +3,14 @@ 1. Head over to https://realfavicongenerator.net/ to add your own favicons. 2. Customize default _includes/custom-head.html in your source directory and insert the given code snippet. + + + + + + + + + + {% endcomment %} diff --git a/docs/_includes/favicon.ico b/docs/_includes/favicon.ico new file mode 100644 index 00000000000..0fdbe7bd000 Binary files /dev/null and b/docs/_includes/favicon.ico differ diff --git a/docs/_includes/head.html b/docs/_includes/head.html index 83ac5326933..8fd31008de7 100644 --- a/docs/_includes/head.html +++ b/docs/_includes/head.html @@ -4,6 +4,7 @@ + {%- include custom-head.html -%} diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..1e50de4c654 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: "FumbleLog"; font-size: 32px; } } diff --git a/docs/diagrams/AddEventObjectDiagram.puml b/docs/diagrams/AddEventObjectDiagram.puml new file mode 100644 index 00000000000..0dabcc9fc2d --- /dev/null +++ b/docs/diagrams/AddEventObjectDiagram.puml @@ -0,0 +1,40 @@ +@startuml +'https://plantuml.com/object-diagram + +!define MySkin +skinparam object { + BackgroundColor LightBlue +} + +object "__model:ModelManager__" as model +object "__addressBook:AddressBook__" as addressBook +object "__:UniquePersonList__" as uniquePersonList +object "__:EventList__" as eventList +object "__newEvent:Meeting__" as newEvent + +object "__meeting1:Meeting__" as meeting1 + +object "__startDate:EventDate__" as startDate +object "__startTime:EventTime__" as startTime +object "__name:EventName__" as name + +object "__meeting2:Meeting__" as meeting2 +object "__Bob:Person__" as Bob +object "__Alice:Person__" as Alice + + + +model --> addressBook +addressBook --> uniquePersonList +addressBook --> eventList +uniquePersonList --> Bob +uniquePersonList --> Alice +eventList --> meeting1 +eventList --> meeting2 +eventList --> newEvent + +meeting1 --> name +meeting1 --> startDate +meeting1 --> startTime + +@enduml diff --git a/docs/diagrams/AddPersonSequenceDiagram.puml b/docs/diagrams/AddPersonSequenceDiagram.puml new file mode 100644 index 00000000000..13fd86c92eb --- /dev/null +++ b/docs/diagrams/AddPersonSequenceDiagram.puml @@ -0,0 +1,79 @@ +@startuml +!include ./style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":AddPersonCommandParser" as AddPersonCommandParser LOGIC_COLOR +participant ":ParserUtil" as ParserUtil LOGIC_COLOR +participant "addCommand:AddPersonCommand" as AddPersonCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "model:Model" as Model MODEL_COLOR +end box + +note left of LogicManager +let {str} be "n/Lee Zheng Jing" +end note + +[-> LogicManager : execute("add_person {str}") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("add_person {str}") +activate AddressBookParser + +create AddPersonCommandParser +AddressBookParser -> AddPersonCommandParser +activate AddPersonCommandParser + +AddPersonCommandParser --> AddressBookParser +deactivate AddPersonCommandParser + +AddressBookParser -> AddPersonCommandParser : parse("{str}") +activate AddPersonCommandParser + +AddPersonCommandParser -> ParserUtil : parseName("Lee Zheng Jing") +activate ParserUtil +ParserUtil --> AddPersonCommandParser : name +deactivate ParserUtil + +create AddPersonCommand +AddPersonCommandParser -> AddPersonCommand : new AddPersonCommand(person) +activate AddPersonCommand +AddPersonCommand --> AddPersonCommandParser : addPersonCommand +deactivate AddPersonCommand + +AddPersonCommandParser --> AddressBookParser : addPersonCommand +deactivate AddPersonCommandParser + +AddressBookParser --> LogicManager : addPersonCommand +deactivate AddressBookParser + +LogicManager -> AddPersonCommand : execute() +activate AddPersonCommand + +AddPersonCommand -> Model : addPerson(person) +activate Model +Model --> AddPersonCommand : person +deactivate Model + +'AddCommand -> AddCommand : addPerson(person) + +AddPersonCommand -> Model : updateGroups() +activate Model +Model --> AddPersonCommand +deactivate Model + +create CommandResult +AddPersonCommand -> CommandResult +activate CommandResult + +CommandResult --> AddPersonCommand +deactivate CommandResult + +[<--LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/AddPersonSequenceDiagram2.puml b/docs/diagrams/AddPersonSequenceDiagram2.puml new file mode 100644 index 00000000000..190e9616585 --- /dev/null +++ b/docs/diagrams/AddPersonSequenceDiagram2.puml @@ -0,0 +1,45 @@ +@startuml +!include style.puml + +mainframe **sd** execute AddPersonCommand + +participant "x:AddPersonCommand" as AddPersonCommand LOGIC_COLOR + +participant ":Model" as Model MODEL_COLOR + +participant ":AddressBook" as AddressBook MODEL_COLOR_T2 + +participant ":UniquePersonList" as UniquePersonList MODEL_COLOR_T3 + + +[-> AddPersonCommand : execute() +activate AddPersonCommand + +AddPersonCommand -> Model : addPerson(person) +activate Model + +Model -> AddressBook : addPerson(person) +activate AddressBook + +AddressBook -> UniquePersonList : add(person) +activate UniquePersonList + +UniquePersonList ---> AddressBook +deactivate UniquePersonList + +AddressBook ---> Model +deactivate AddressBook + +Model -> Model : updateFilteredPersonList() +activate Model + +Model ---> Model +deactivate Model + +Model --> AddPersonCommand +deactivate Model + +[<-- AddPersonCommand +deactivate AddPersonCommand + +@enduml diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index 48b6cc4333c..1fdb859f135 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -8,10 +8,10 @@ Participant ":Logic" as logic LOGIC_COLOR Participant ":Model" as model MODEL_COLOR Participant ":Storage" as storage STORAGE_COLOR -user -[USER_COLOR]> ui : "delete 1" +user -[USER_COLOR]> ui : "delete_person 1" activate ui UI_COLOR -ui -[UI_COLOR]> logic : execute("delete 1") +ui -[UI_COLOR]> logic : execute("delete_person 1") activate logic LOGIC_COLOR logic -[LOGIC_COLOR]> model : deletePerson(p) diff --git a/docs/diagrams/AssignGroupsSequenceDiagram.puml b/docs/diagrams/AssignGroupsSequenceDiagram.puml new file mode 100644 index 00000000000..38e0d3a34f9 --- /dev/null +++ b/docs/diagrams/AssignGroupsSequenceDiagram.puml @@ -0,0 +1,58 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box EditEventCommand LOGIC_COLOR_T1 +participant ":EditEventCommand" as EditEventCommand LOGIC_COLOR +participant "m:Meeting" as Meeting REMIND_COMMAND_PARSER_COLOR +participant "comd:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "model:Model" as Model MODEL_COLOR +end box + +box AddressBook +participant "ab:AddressBook" as AddressBook ADDRESS_BOOK_PARSER_COLOR +end box + +[-> EditEventCommand : execute(model) +activate EditEventCommand + +EditEventCommand -> EditEventCommand : createEditedMeeting(...) +activate EditEventCommand + +EditEventCommand -> EditEventCommand : handleEditAssignGroups(...) + +create Meeting +EditEventCommand -[ADDRESS_BOOK_PARSER_COLOR]> Meeting +activate Meeting + +Meeting --> EditEventCommand +deactivate EditEventCommand +deactivate Meeting + +EditEventCommand -> Model : setEvent(m) +activate Model + +Model --> AddressBook : setEvent(m) +activate AddressBook + +AddressBook --> AddressBook : removePersonsInGroups(m) +activate AddressBook +deactivate AddressBook +AddressBook --> Model +deactivate AddressBook + +Model --> EditEventCommand +deactivate Model +deactivate Meeting + +create CommandResult +EditEventCommand -[LOGIC_COLOR]> CommandResult +activate CommandResult + +CommandResult --> EditEventCommand +[<--EditEventCommand : comd +deactivate EditEventCommand +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..b94a791cb32 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -5,17 +5,24 @@ skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR AddressBook *-right-> "1" UniquePersonList -AddressBook *-right-> "1" UniqueTagList -UniqueTagList -[hidden]down- UniquePersonList -UniqueTagList -[hidden]down- UniquePersonList +AddressBook *-left-> "1" EventList -UniqueTagList -right-> "*" Tag -UniquePersonList -right-> Person +UniquePersonList --> "*" Person +EventList --> "*" Event -Person -up-> "*" Tag +Person --> "*" Group -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address +Person *-right-> "1" Name +Person *--> "0..1" Phone +Person *--> "0..1" Email +Person *--> "0..1" Address +Person *--> "0..1" Birthday +Person *--> "0..1" Remark + +Event *-left-> "1" EventName +Event *--> "1" EventType +Event *--> "1..2" EventDate +Event *--> "0..2" EventTime +Event *--> "*" Group +Event *-> "*" Person @enduml diff --git a/docs/diagrams/CommandFlowActivityDiagram.puml b/docs/diagrams/CommandFlowActivityDiagram.puml new file mode 100644 index 00000000000..efdf813953b --- /dev/null +++ b/docs/diagrams/CommandFlowActivityDiagram.puml @@ -0,0 +1,37 @@ +@startuml +'https://plantuml.com/activity-diagram-beta + +start +#FFFFAA:User creates command request; +#FFFFAA:Parser parses command; +if () then ([Command parsed without error]) + #FFFFAA:Corresponding command is created; + #FFFFAA:Command is executed; + + if () then ([Command executed without error]) + if () then ([Is Exit Command]) + stop + else ([else]) + endif + + if () then ([Models require updates]) + #FFFFAA:Models are updated; + #FFFFAA:Storage is updated; + + else ([else]) + + endif + #FFFFAA:Ui is updated; + #FFFFAA:Command response is displayed; + else ([Command executed with error]) + #FFFFAA:Error message displayed; + stop + + endif + stop +else ([Command parsed with error]) + #FFFFAA:Error message displayed; + stop + + +@enduml diff --git a/docs/diagrams/CommandsParserSequenceDiagram.puml b/docs/diagrams/CommandsParserSequenceDiagram.puml new file mode 100644 index 00000000000..b065996445d --- /dev/null +++ b/docs/diagrams/CommandsParserSequenceDiagram.puml @@ -0,0 +1,66 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":XYZCommandParser" as XYZCommandParser LOGIC_COLOR +participant "c:XYZCommand" as XYZCommand LOGIC_COLOR +participant "result:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("xyz ...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("xyz ...") +activate AddressBookParser + +create XYZCommandParser +AddressBookParser -> XYZCommandParser +activate XYZCommandParser + +XYZCommandParser --> AddressBookParser +deactivate XYZCommandParser + +AddressBookParser -> XYZCommandParser : parse("...") +activate XYZCommandParser + +create XYZCommand +XYZCommandParser -> XYZCommand +activate XYZCommand + +XYZCommand --> XYZCommandParser +deactivate XYZCommand + +XYZCommandParser --> AddressBookParser : c +deactivate XYZCommandParser + +XYZCommandParser -[hidden]-> AddressBookParser +destroy XYZCommandParser + +AddressBookParser --> LogicManager : c +deactivate AddressBookParser + +LogicManager -> XYZCommand : execute() + +activate XYZCommand +ref over XYZCommand, Model : execute XYZCommand + +create CommandResult +XYZCommand -> CommandResult +activate CommandResult + +CommandResult --> XYZCommand +deactivate CommandResult + +XYZCommand --> LogicManager : result +deactivate XYZCommand + +[<-- LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/diagrams/DeletePersonSequenceDiagram.puml b/docs/diagrams/DeletePersonSequenceDiagram.puml new file mode 100644 index 00000000000..4b909fb5740 --- /dev/null +++ b/docs/diagrams/DeletePersonSequenceDiagram.puml @@ -0,0 +1,37 @@ +@startuml +!include style.puml + +mainframe **sd** execute DeletePersonCommand + +participant "d:DeletePersonCommand" as DeletePersonCommand LOGIC_COLOR + +participant ":Model" as Model MODEL_COLOR + +participant ":AddressBook" as TaskListBook MODEL_COLOR_T2 + +participant ":UniquePersonList" as TaskList MODEL_COLOR_T3 + +[-> DeletePersonCommand : execute() +activate DeletePersonCommand + +DeletePersonCommand -> Model : deletePerson(person) + +Model -> TaskListBook : removePerson(person) +activate TaskListBook + +TaskListBook -> TaskList : remove(person) +activate TaskList + +TaskList --> TaskListBook +deactivate TaskList + +TaskListBook ---> Model +deactivate TaskListBook + +Model --> DeletePersonCommand +deactivate Model + +[<-- DeletePersonCommand +deactivate DeletePersonCommand + +@enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 40ea6c9dc4c..a0d1beb9d8f 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -5,8 +5,8 @@ skinparam ArrowFontStyle plain box Logic LOGIC_COLOR_T1 participant ":LogicManager" as LogicManager LOGIC_COLOR participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR -participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR -participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR +participant ":DeletePersonCommandParser" as DeletePersonCommandParser LOGIC_COLOR +participant "d:DeletePersonCommand" as DeletePersonCommand LOGIC_COLOR participant ":CommandResult" as CommandResult LOGIC_COLOR end box @@ -14,56 +14,56 @@ box Model MODEL_COLOR_T1 participant ":Model" as Model MODEL_COLOR end box -[-> LogicManager : execute("delete 1") +[-> LogicManager : execute("delete_person 1") activate LogicManager -LogicManager -> AddressBookParser : parseCommand("delete 1") +LogicManager -> AddressBookParser : parseCommand("delete_person 1") activate AddressBookParser -create DeleteCommandParser -AddressBookParser -> DeleteCommandParser -activate DeleteCommandParser +create DeletePersonCommandParser +AddressBookParser -> DeletePersonCommandParser +activate DeletePersonCommandParser -DeleteCommandParser --> AddressBookParser -deactivate DeleteCommandParser +DeletePersonCommandParser --> AddressBookParser +deactivate DeletePersonCommandParser -AddressBookParser -> DeleteCommandParser : parse("1") -activate DeleteCommandParser +AddressBookParser -> DeletePersonCommandParser : parse("1") +activate DeletePersonCommandParser -create DeleteCommand -DeleteCommandParser -> DeleteCommand -activate DeleteCommand +create DeletePersonCommand +DeletePersonCommandParser -> DeletePersonCommand +activate DeletePersonCommand -DeleteCommand --> DeleteCommandParser : d -deactivate DeleteCommand +DeletePersonCommand --> DeletePersonCommandParser : d +deactivate DeletePersonCommand -DeleteCommandParser --> AddressBookParser : d -deactivate DeleteCommandParser +DeletePersonCommandParser --> AddressBookParser : d +deactivate DeletePersonCommandParser 'Hidden arrow to position the destroy marker below the end of the activation bar. -DeleteCommandParser -[hidden]-> AddressBookParser -destroy DeleteCommandParser +DeletePersonCommandParser -[hidden]-> AddressBookParser +destroy DeletePersonCommandParser AddressBookParser --> LogicManager : d deactivate AddressBookParser -LogicManager -> DeleteCommand : execute() -activate DeleteCommand +LogicManager -> DeletePersonCommand : execute() +activate DeletePersonCommand -DeleteCommand -> Model : deletePerson(1) +DeletePersonCommand -> Model : deletePerson(1) activate Model -Model --> DeleteCommand +Model --> DeletePersonCommand deactivate Model create CommandResult -DeleteCommand -> CommandResult +DeletePersonCommand -> CommandResult activate CommandResult -CommandResult --> DeleteCommand +CommandResult --> DeletePersonCommand deactivate CommandResult -DeleteCommand --> LogicManager : result -deactivate DeleteCommand +DeletePersonCommand --> LogicManager : result +deactivate DeletePersonCommand [<--LogicManager deactivate LogicManager diff --git a/docs/diagrams/EditEventObjectDiagram.puml b/docs/diagrams/EditEventObjectDiagram.puml new file mode 100644 index 00000000000..994f1676595 --- /dev/null +++ b/docs/diagrams/EditEventObjectDiagram.puml @@ -0,0 +1,37 @@ +@startuml +'https://plantuml.com/object-diagram + +!define MySkin +skinparam object { + BackgroundColor LightBlue +} + +object "__model:ModelManager__" as model +object "__addressBook:AddressBook__" as addressBook +object "__:UniquePersonList__" as uniquePersonList +object "__:EventList__" as eventList + +object "__TP meeting:Meeting__" as tp_meeting + +object "__Bob:Person__" as Bob +object "__Alice:Person__" as Alice +object "__John:Person__" as John + +object "__CS2103T:Group__" as cs2103t + +model --> addressBook +addressBook --> uniquePersonList +addressBook --> eventList +uniquePersonList --> Bob +uniquePersonList --> Alice +uniquePersonList --> John + +Alice --> cs2103t +Bob --> cs2103t + +eventList --> tp_meeting + +tp_meeting --> cs2103t +tp_meeting --> John + +@enduml diff --git a/docs/diagrams/EventClassDiagram.puml b/docs/diagrams/EventClassDiagram.puml new file mode 100644 index 00000000000..0988d816329 --- /dev/null +++ b/docs/diagrams/EventClassDiagram.puml @@ -0,0 +1,24 @@ +@startuml +!include ./style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Class EventList +Class "{abstract}\nEvent" as Event +Class Meeting extends Event +Class EventType +Class EventName +Class EventDate +Class EventTime +Class Person +Class Group + +EventList *--> "*" Event +Event *--> "1" EventName +Event *--> "1" EventType +Event *--> "1..2" EventDate +Event *--> "0..2" EventTime +Event *--> "*" Person +Event *--> "*" Group +@enduml diff --git a/docs/diagrams/FindAllSequenceDiagram.puml b/docs/diagrams/FindAllSequenceDiagram.puml new file mode 100644 index 00000000000..6ab5e66a9d9 --- /dev/null +++ b/docs/diagrams/FindAllSequenceDiagram.puml @@ -0,0 +1,116 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as Logic LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser ADDRESS_BOOK_PARSER_COLOR +participant ":FindAllCommandParser" as FindAllCommandParser ADDRESS_BOOK_PARSER_COLOR +participant "pp:PersonNameOrGroupContainsKeywordsPredicate" as PersonNameOrGroupContainsKeywordsPredicate PERSON_CONTAIN_KEYWORD_COLOR +participant "ep:EventNameOrGroupContainsKeywordsPredicate" as EventNameOrGroupContainsKeywordsPredicate EVENT_CONTAIN_KEYWORD_COLOR +participant "fa:FindAllCommand" as FindAllCommand FIND_COMMAND_COLOR +participant ":CommandResult" as CommandResult COMMAND_RESULT_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> Logic : execute(...) +activate Logic + +Logic -[LOGIC_COLOR]> AddressBookParser : parseCommand(...) +activate AddressBookParser + +create FindAllCommandParser +AddressBookParser -[ADDRESS_BOOK_PARSER_COLOR]> FindAllCommandParser +activate FindAllCommandParser + +FindAllCommandParser --> AddressBookParser +deactivate FindAllCommandParser + +AddressBookParser -[ADDRESS_BOOK_PARSER_COLOR]> FindAllCommandParser : parse(...) +activate FindAllCommandParser + +create PersonNameOrGroupContainsKeywordsPredicate +FindAllCommandParser -[FIND_COMMAND_PARSER_COLOR]> PersonNameOrGroupContainsKeywordsPredicate +activate PersonNameOrGroupContainsKeywordsPredicate + +PersonNameOrGroupContainsKeywordsPredicate --> FindAllCommandParser +deactivate PersonNameOrGroupContainsKeywordsPredicate + +create EventNameOrGroupContainsKeywordsPredicate +FindAllCommandParser -[FIND_COMMAND_PARSER_COLOR]> EventNameOrGroupContainsKeywordsPredicate +activate EventNameOrGroupContainsKeywordsPredicate + +EventNameOrGroupContainsKeywordsPredicate --> FindAllCommandParser +deactivate EventNameOrGroupContainsKeywordsPredicate + +create FindAllCommand +FindAllCommandParser -[FIND_COMMAND_PARSER_COLOR]> FindAllCommand : new FindAllCommand(pp, ep) +activate FindAllCommand + +FindAllCommand --> FindAllCommandParser : fa +deactivate FindAllCommand + +FindAllCommandParser --> AddressBookParser : fa +deactivate FindAllCommandParser +FindAllCommandParser -[hidden]-> AddressBookParser +destroy FindAllCommandParser + +AddressBookParser --> Logic : fa +deactivate AddressBookParser + +Logic -> FindAllCommand : execute() +activate FindAllCommand + +FindAllCommand -[FIND_COMMAND_COLOR]> Model : getFullPersonList() +activate Model + +Model --> FindAllCommand : fullPersonList +deactivate Model + +FindAllCommand -[FIND_COMMAND_COLOR]> EventNameOrGroupContainsKeywordsPredicate : setPersonList(fullPersonList) +activate EventNameOrGroupContainsKeywordsPredicate + +EventNameOrGroupContainsKeywordsPredicate --> FindAllCommand +deactivate EventNameOrGroupContainsKeywordsPredicate + +FindAllCommand -[FIND_COMMAND_COLOR]> Model : updateFilteredPersonList(p -> true) +activate Model + +Model --> FindAllCommand +deactivate Model + +FindAllCommand -[FIND_COMMAND_COLOR]> Model : updateFilteredEventList(e -> true) +activate Model + +Model --> FindAllCommand +deactivate Model + +FindAllCommand -[FIND_COMMAND_COLOR]> Model : updateFilteredEventList(ep) +activate Model + +Model --> FindAllCommand +deactivate Model + +FindAllCommand -[FIND_COMMAND_COLOR]> Model : updateFilteredPersonList(pp) +activate Model + +Model --> FindAllCommand +deactivate Model + +create CommandResult +FindAllCommand -[FIND_COMMAND_COLOR]> CommandResult +activate CommandResult + +CommandResult --> FindAllCommand : result +deactivate CommandResult + +FindAllCommand --> Logic : result +deactivate FindAllCommand + +[<--Logic : result +deactivate Logic + +@enduml diff --git a/docs/diagrams/FindEventSequenceDiagram.puml b/docs/diagrams/FindEventSequenceDiagram.puml new file mode 100644 index 00000000000..d8628b81139 --- /dev/null +++ b/docs/diagrams/FindEventSequenceDiagram.puml @@ -0,0 +1,91 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as Logic LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser ADDRESS_BOOK_PARSER_COLOR +participant ":FindEventCommandParser" as FindEventCommandParser ADDRESS_BOOK_PARSER_COLOR +participant "p:EventNameOrGroupContainsKeywordsPredicate" as EventNameOrGroupContainsKeywordsPredicate EVENT_CONTAIN_KEYWORD_COLOR +participant "fe:FindEventCommand" as FindEventCommand FIND_COMMAND_COLOR +participant ":CommandResult" as CommandResult COMMAND_RESULT_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> Logic : execute(...) +activate Logic + +Logic -[LOGIC_COLOR]> AddressBookParser : parseCommand(...) +activate AddressBookParser + +create FindEventCommandParser +AddressBookParser -[ADDRESS_BOOK_PARSER_COLOR]> FindEventCommandParser +activate FindEventCommandParser + +FindEventCommandParser --> AddressBookParser +deactivate FindEventCommandParser + +AddressBookParser -[ADDRESS_BOOK_PARSER_COLOR]> FindEventCommandParser : parse(...) +activate FindEventCommandParser + +create EventNameOrGroupContainsKeywordsPredicate +FindEventCommandParser -[FIND_COMMAND_PARSER_COLOR]> EventNameOrGroupContainsKeywordsPredicate +activate EventNameOrGroupContainsKeywordsPredicate + +EventNameOrGroupContainsKeywordsPredicate --> FindEventCommandParser +deactivate EventNameOrGroupContainsKeywordsPredicate + +create FindEventCommand +FindEventCommandParser -[FIND_COMMAND_PARSER_COLOR]> FindEventCommand : new FindEventCommand(p) +activate FindEventCommand + +FindEventCommand --> FindEventCommandParser : fe +deactivate FindEventCommand + +FindEventCommandParser --> AddressBookParser : fe +deactivate FindEventCommandParser +FindEventCommandParser -[hidden]-> AddressBookParser +destroy FindEventCommandParser + +AddressBookParser --> Logic : fe +deactivate AddressBookParser + +Logic -> FindEventCommand : execute() +activate FindEventCommand + +FindEventCommand -[FIND_COMMAND_COLOR]> Model : getFullPersonList() +activate Model + +Model --> FindEventCommand : fullPersonList +deactivate Model + +FindEventCommand -[FIND_COMMAND_COLOR]> EventNameOrGroupContainsKeywordsPredicate : setPersonList(fullPersonList) +activate EventNameOrGroupContainsKeywordsPredicate + +EventNameOrGroupContainsKeywordsPredicate --> FindEventCommand +deactivate EventNameOrGroupContainsKeywordsPredicate + +FindEventCommand -[FIND_COMMAND_COLOR]> Model : updateFilteredEventList(p) +activate Model + +Model --> FindEventCommand +deactivate Model + +create CommandResult +FindEventCommand -[FIND_COMMAND_COLOR]> CommandResult +activate CommandResult + +CommandResult --> FindEventCommand : result +deactivate CommandResult + +FindEventCommand --> Logic : result +deactivate FindEventCommand + +[<--Logic : result +deactivate Logic + +@enduml diff --git a/docs/diagrams/FindPersonSequenceDiagram.puml b/docs/diagrams/FindPersonSequenceDiagram.puml new file mode 100644 index 00000000000..a2551515622 --- /dev/null +++ b/docs/diagrams/FindPersonSequenceDiagram.puml @@ -0,0 +1,78 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as Logic LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser ADDRESS_BOOK_PARSER_COLOR +participant ":FindPersonCommandParser" as FindPersonCommandParser ADDRESS_BOOK_PARSER_COLOR +participant "p:PersonNameOrGroupContainsKeywordsPredicate" as PersonNameOrGroupContainsKeywordsPredicate PERSON_CONTAIN_KEYWORD_COLOR +participant "fp:FindPersonCommand" as FindPersonCommand FIND_COMMAND_COLOR +participant ":CommandResult" as CommandResult COMMAND_RESULT_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> Logic : execute(...) +activate Logic + +Logic -[LOGIC_COLOR]> AddressBookParser : parseCommand(...) +activate AddressBookParser + +create FindPersonCommandParser +AddressBookParser -[ADDRESS_BOOK_PARSER_COLOR]> FindPersonCommandParser +activate FindPersonCommandParser + +FindPersonCommandParser --> AddressBookParser +deactivate FindPersonCommandParser + +AddressBookParser -[ADDRESS_BOOK_PARSER_COLOR]> FindPersonCommandParser : parse(...) +activate FindPersonCommandParser + +create PersonNameOrGroupContainsKeywordsPredicate +FindPersonCommandParser -[FIND_COMMAND_PARSER_COLOR]> PersonNameOrGroupContainsKeywordsPredicate +activate PersonNameOrGroupContainsKeywordsPredicate + +PersonNameOrGroupContainsKeywordsPredicate --> FindPersonCommandParser +deactivate PersonNameOrGroupContainsKeywordsPredicate + +create FindPersonCommand +FindPersonCommandParser -[FIND_COMMAND_PARSER_COLOR]> FindPersonCommand : new FindPersonCommand(p) +activate FindPersonCommand + +FindPersonCommand --> FindPersonCommandParser : fp +deactivate FindPersonCommand + +FindPersonCommandParser --> AddressBookParser : fp +deactivate FindPersonCommandParser +FindPersonCommandParser -[hidden]-> AddressBookParser +destroy FindPersonCommandParser + +AddressBookParser --> Logic : fp +deactivate AddressBookParser + +Logic -> FindPersonCommand : execute() +activate FindPersonCommand + +FindPersonCommand -[FIND_COMMAND_COLOR]> Model : updateFilteredPersonList(p) +activate Model + +Model --> FindPersonCommand +deactivate Model + +create CommandResult +FindPersonCommand -[FIND_COMMAND_COLOR]> CommandResult +activate CommandResult + +CommandResult --> FindPersonCommand : result +deactivate CommandResult + +FindPersonCommand --> Logic : result +deactivate FindPersonCommand + +[<--Logic : result +deactivate Logic + +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..2d3707f8347 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -18,7 +18,17 @@ Class Address Class Email Class Name Class Phone -Class Tag +Class Group +Class Birthday +Class Remark + +Class EventList +Class "{abstract}\nEvent" as Event +Class EventDate +Class EventName +Class EventTime +Class EventType +Class Meeting Class I #FFFFFF } @@ -35,13 +45,24 @@ ModelManager -left-> "1" AddressBook ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList +AddressBook --> "1" UniquePersonList +AddressBook --> "1" EventList UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag +EventList --> "~* all" Event +Event <|-- Meeting +Person --> "1" Name +Person --> "0..1" Phone +Person --> "0..1" Email +Person --> "0..1" Address +Person --> "*" Group +Person --> "0..1" Birthday +Person --> "0..1" Remark +Event --> "1" EventName +Event --> "1..2" EventDate +Event --> "1" EventType +Event --> "0..2" EventTime +Event --> "*" Group +Event --> "*" Person Person -[hidden]up--> I UniquePersonList -[hidden]right-> I @@ -51,4 +72,5 @@ Phone -[hidden]right-> Address Address -[hidden]right-> Email ModelManager --> "~* filtered" Person +ModelManager --> "~* filtered" Event @enduml diff --git a/docs/diagrams/PersonClassDiagram.puml b/docs/diagrams/PersonClassDiagram.puml new file mode 100644 index 00000000000..32ac0e79fd9 --- /dev/null +++ b/docs/diagrams/PersonClassDiagram.puml @@ -0,0 +1,28 @@ +@startuml +!include ./style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Class Person +Class Address +Class Email +Class Name +Class Phone +Class Group +Class Birthday +Class Remark + +Person *--> "1" Name +Person *--> "0..1" Phone +Person *--> "0..1" Email +Person *--> "0..1" Address +Person *--> "*" Group +Person *--> "0..1" Birthday +Person *--> "0..1" Remark + +Name -[hidden]right-> Phone +Phone -[hidden]right-> Address +Address -[hidden]right-> Email + +@enduml` diff --git a/docs/diagrams/RemindSequenceDiagram.puml b/docs/diagrams/RemindSequenceDiagram.puml new file mode 100644 index 00000000000..e56ec790480 --- /dev/null +++ b/docs/diagrams/RemindSequenceDiagram.puml @@ -0,0 +1,92 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as Logic LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser ADDRESS_BOOK_PARSER_COLOR +participant ":RemindCommandParser" as RemindCommandParser REMIND_COMMAND_PARSER_COLOR +participant "bp:BirthdayWithinDaysPredicate" as BirthdayWithinDays BIRTHDAY_WITHIN_DAYS_COLOR +participant "ep:EventWithinDaysPredicate" as EventWithinDays EVENT_WITHIN_DAYS_COLOR +participant "r:RemindCommand" as RemindCommand REMIND_COMMAND_COLOR +participant ":CommandResult" as CommandResult COMMAND_RESULT_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> Logic : execute("remind 1") +activate Logic + +Logic -[LOGIC_COLOR]> AddressBookParser : parseCommand("remind 1") +activate AddressBookParser + +create RemindCommandParser +AddressBookParser -[ADDRESS_BOOK_PARSER_COLOR]> RemindCommandParser +activate RemindCommandParser + +RemindCommandParser --> AddressBookParser +deactivate RemindCommandParser + +AddressBookParser -[ADDRESS_BOOK_PARSER_COLOR]> RemindCommandParser : parse("1') +activate RemindCommandParser + +create BirthdayWithinDays +RemindCommandParser -[REMIND_COMMAND_PARSER_COLOR]> BirthdayWithinDays +activate BirthdayWithinDays + +BirthdayWithinDays --> RemindCommandParser +deactivate BirthdayWithinDays + +create EventWithinDays +RemindCommandParser -[REMIND_COMMAND_PARSER_COLOR]> EventWithinDays +activate EventWithinDays + +EventWithinDays --> RemindCommandParser +deactivate EventWithinDays + +create RemindCommand +RemindCommandParser -[REMIND_COMMAND_PARSER_COLOR]> RemindCommand : new RemindCommand(bp, ep, 1) +activate RemindCommand + +RemindCommand --> RemindCommandParser : r +deactivate RemindCommand + +RemindCommandParser --> AddressBookParser : r +deactivate RemindCommandParser +RemindCommandParser -[hidden]-> AddressBookParser +destroy RemindCommandParser + +AddressBookParser --> Logic : r +deactivate AddressBookParser + +Logic -> RemindCommand : execute() +activate RemindCommand + +RemindCommand -[REMIND_COMMAND_COLOR]> Model : updateFilteredPersonList(bp) +activate Model + +Model --> RemindCommand +deactivate Model + +RemindCommand -[REMIND_COMMAND_COLOR]> Model : updateFilteredEventList(ep) +activate Model + +Model --> RemindCommand +deactivate Model + +create CommandResult +RemindCommand -[REMIND_COMMAND_COLOR]> CommandResult +activate CommandResult + +CommandResult --> RemindCommand +deactivate CommandResult + +RemindCommand --> Logic : result +deactivate RemindCommand + +[<--Logic +deactivate Logic + +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..6d8ec0280f0 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -19,7 +19,9 @@ Class "<>\nAddressBookStorage" as AddressBookStorage Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson -Class JsonAdaptedTag +Class JsonAdaptedEvent +Class JsonAdaptedGroup +Class JsonAdaptedName } } @@ -38,6 +40,9 @@ JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson -JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonSerializableAddressBook --> "*" JsonAdaptedEvent +JsonAdaptedPerson --> "*" JsonAdaptedGroup +JsonAdaptedEvent --> "*" JsonAdaptedGroup +JsonAdaptedEvent --> "*" JsonAdaptedName @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..fc96d155fde 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -15,6 +15,8 @@ Class PersonListPanel Class PersonCard Class StatusBarFooter Class CommandBox +Class EventListPanel +Class EventCard } package Model <> { @@ -33,10 +35,12 @@ UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" EventListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow PersonListPanel -down-> "*" PersonCard +EventListPanel -down-> "*" EventCard MainWindow -left-|> UiPart @@ -44,13 +48,17 @@ ResultDisplay --|> UiPart CommandBox --|> UiPart PersonListPanel --|> UiPart PersonCard --|> UiPart +EventListPanel --|> UiPart +EventCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +EventCard ..> Model PersonCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic +EventListPanel -[hidden]left- PersonListPanel PersonListPanel -[hidden]left- HelpWindow HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay diff --git a/docs/diagrams/UndoRedoState1.puml b/docs/diagrams/UndoRedoState1.puml index 5a41e9e1651..a39d7ad083d 100644 --- a/docs/diagrams/UndoRedoState1.puml +++ b/docs/diagrams/UndoRedoState1.puml @@ -4,7 +4,7 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 skinparam ClassBackgroundColor #FFFFAA -title After command "delete 5" +title After command "delete_person 5" package States <> { class State1 as "ab0:AddressBook" diff --git a/docs/diagrams/UndoRedoState2.puml b/docs/diagrams/UndoRedoState2.puml index ad32fce1b0b..716cadbb3de 100644 --- a/docs/diagrams/UndoRedoState2.puml +++ b/docs/diagrams/UndoRedoState2.puml @@ -4,7 +4,7 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 skinparam ClassBackgroundColor #FFFFAA -title After command "add n/David" +title After command "add_person n/David" package States <> { class State1 as "ab0:AddressBook" diff --git a/docs/diagrams/UndoRedoState4.puml b/docs/diagrams/UndoRedoState4.puml index 2bc631ffcd0..260ba9e2091 100644 --- a/docs/diagrams/UndoRedoState4.puml +++ b/docs/diagrams/UndoRedoState4.puml @@ -4,7 +4,7 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 skinparam ClassBackgroundColor #FFFFAA -title After command "list" +title After command "list_all" package States <> { class State1 as "ab0:AddressBook" diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index f7d7347ae84..e3f4bb68f2e 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -31,6 +31,17 @@ !define STORAGE_COLOR_T3 #806600 !define STORAGE_COLOR_T2 #544400 +!define ADDRESS_BOOK_PARSER_COLOR #166800 +!define REMIND_COMMAND_PARSER_COLOR #544400 +!define REMIND_COMMAND_COLOR #6A6ADC +!define FIND_COMMAND_PARSER_COLOR #A85F42 +!define FIND_COMMAND_COLOR #D47B15 +!define BIRTHDAY_WITHIN_DAYS_COLOR #F97181 +!define EVENT_WITHIN_DAYS_COLOR #E41F36 +!define PERSON_CONTAIN_KEYWORD_COLOR #159BD4 +!define EVENT_CONTAIN_KEYWORD_COLOR #4287F5 +!define COMMAND_RESULT_COLOR #1D8900 + !define USER_COLOR #000000 skinparam Package { diff --git a/docs/images/AddEventObjectDiagram.png b/docs/images/AddEventObjectDiagram.png new file mode 100644 index 00000000000..277b4811e6a Binary files /dev/null and b/docs/images/AddEventObjectDiagram.png differ diff --git a/docs/images/AddPersonSequenceDiagram.png b/docs/images/AddPersonSequenceDiagram.png new file mode 100644 index 00000000000..6376934932a Binary files /dev/null and b/docs/images/AddPersonSequenceDiagram.png differ diff --git a/docs/images/AddPersonSequenceDiagram2.png b/docs/images/AddPersonSequenceDiagram2.png new file mode 100644 index 00000000000..131460ccb24 Binary files /dev/null and b/docs/images/AddPersonSequenceDiagram2.png differ diff --git a/docs/images/Addperson.png b/docs/images/Addperson.png new file mode 100644 index 00000000000..b65158a0a99 Binary files /dev/null and b/docs/images/Addperson.png differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 37ad06a2803..77c8fa99ecd 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/AssignGroupsSequenceDiagram.png b/docs/images/AssignGroupsSequenceDiagram.png new file mode 100644 index 00000000000..181ab8347a4 Binary files /dev/null and b/docs/images/AssignGroupsSequenceDiagram.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..59d2d672c91 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/CommandFlowActivityDiagram.png b/docs/images/CommandFlowActivityDiagram.png new file mode 100644 index 00000000000..df1298bc187 Binary files /dev/null and b/docs/images/CommandFlowActivityDiagram.png differ diff --git a/docs/images/CommandsParserSequenceDiagram.png b/docs/images/CommandsParserSequenceDiagram.png new file mode 100644 index 00000000000..d4f9e646693 Binary files /dev/null and b/docs/images/CommandsParserSequenceDiagram.png differ diff --git a/docs/images/DeletePerson.png b/docs/images/DeletePerson.png new file mode 100644 index 00000000000..7ad56b494bb Binary files /dev/null and b/docs/images/DeletePerson.png differ diff --git a/docs/images/DeletePersonSequenceDiagram.png b/docs/images/DeletePersonSequenceDiagram.png new file mode 100644 index 00000000000..2e6c0c7ffb9 Binary files /dev/null and b/docs/images/DeletePersonSequenceDiagram.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index e186f7ba096..5d08ab4618e 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/DuplicatePersonInDifferentGroups.png b/docs/images/DuplicatePersonInDifferentGroups.png new file mode 100644 index 00000000000..91f8fa71253 Binary files /dev/null and b/docs/images/DuplicatePersonInDifferentGroups.png differ diff --git a/docs/images/EditEventObjectDiagram.png b/docs/images/EditEventObjectDiagram.png new file mode 100644 index 00000000000..f9f5f530517 Binary files /dev/null and b/docs/images/EditEventObjectDiagram.png differ diff --git a/docs/images/Editperson.png b/docs/images/Editperson.png new file mode 100644 index 00000000000..351a68bff08 Binary files /dev/null and b/docs/images/Editperson.png differ diff --git a/docs/images/EventClassDiagram.png b/docs/images/EventClassDiagram.png new file mode 100644 index 00000000000..361a7c6f401 Binary files /dev/null and b/docs/images/EventClassDiagram.png differ diff --git a/docs/images/Eventadd.png b/docs/images/Eventadd.png new file mode 100644 index 00000000000..af8a7349f5e Binary files /dev/null and b/docs/images/Eventadd.png differ diff --git a/docs/images/Eventdelete.png b/docs/images/Eventdelete.png new file mode 100644 index 00000000000..b02812c15c2 Binary files /dev/null and b/docs/images/Eventdelete.png differ diff --git a/docs/images/Eventedit.png b/docs/images/Eventedit.png new file mode 100644 index 00000000000..e808acd0182 Binary files /dev/null and b/docs/images/Eventedit.png differ diff --git a/docs/images/Eventfind.png b/docs/images/Eventfind.png new file mode 100644 index 00000000000..3d72c49d288 Binary files /dev/null and b/docs/images/Eventfind.png differ diff --git a/docs/images/ExpiredEvent.png b/docs/images/ExpiredEvent.png new file mode 100644 index 00000000000..6d3f19bbf7b Binary files /dev/null and b/docs/images/ExpiredEvent.png differ diff --git a/docs/images/FindAllSequenceDiagram.png b/docs/images/FindAllSequenceDiagram.png new file mode 100644 index 00000000000..cab76632c3b Binary files /dev/null and b/docs/images/FindAllSequenceDiagram.png differ diff --git a/docs/images/FindEventSequenceDiagram.png b/docs/images/FindEventSequenceDiagram.png new file mode 100644 index 00000000000..390dfce08ff Binary files /dev/null and b/docs/images/FindEventSequenceDiagram.png differ diff --git a/docs/images/FindPersonSequenceDiagram.png b/docs/images/FindPersonSequenceDiagram.png new file mode 100644 index 00000000000..51a05cce73b Binary files /dev/null and b/docs/images/FindPersonSequenceDiagram.png differ diff --git a/docs/images/Findall.png b/docs/images/Findall.png new file mode 100644 index 00000000000..cf33de78887 Binary files /dev/null and b/docs/images/Findall.png differ diff --git a/docs/images/Helptab.png b/docs/images/Helptab.png new file mode 100644 index 00000000000..4b8959c04c5 Binary files /dev/null and b/docs/images/Helptab.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..14e6857f72b 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/PersonClassDiagram.png b/docs/images/PersonClassDiagram.png new file mode 100644 index 00000000000..6d23981fbae Binary files /dev/null and b/docs/images/PersonClassDiagram.png differ diff --git a/docs/images/Remind.png b/docs/images/Remind.png new file mode 100644 index 00000000000..695d2059ecb Binary files /dev/null and b/docs/images/Remind.png differ diff --git a/docs/images/RemindSequenceDiagram.png b/docs/images/RemindSequenceDiagram.png new file mode 100644 index 00000000000..ec8eb425975 Binary files /dev/null and b/docs/images/RemindSequenceDiagram.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..3b382708309 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..1b4af136648 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 11f06d68671..cb1cb6c8761 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoRedoState1.png b/docs/images/UndoRedoState1.png index 2d3ad09c047..aabbd21c78e 100644 Binary files a/docs/images/UndoRedoState1.png and b/docs/images/UndoRedoState1.png differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png index 20853694e03..0d9118c06cf 100644 Binary files a/docs/images/UndoRedoState2.png and b/docs/images/UndoRedoState2.png differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png index 46dfae78c94..be6aafcb977 100644 Binary files a/docs/images/UndoRedoState4.png and b/docs/images/UndoRedoState4.png differ diff --git a/docs/images/distractedcat.png b/docs/images/distractedcat.png new file mode 100644 index 00000000000..e97656b9a40 Binary files /dev/null and b/docs/images/distractedcat.png differ diff --git a/docs/images/favicon/android-chrome-192x192.png b/docs/images/favicon/android-chrome-192x192.png new file mode 100644 index 00000000000..29ed15b309f Binary files /dev/null and b/docs/images/favicon/android-chrome-192x192.png differ diff --git a/docs/images/favicon/android-chrome-512x512.png b/docs/images/favicon/android-chrome-512x512.png new file mode 100644 index 00000000000..fa2ea79eb08 Binary files /dev/null and b/docs/images/favicon/android-chrome-512x512.png differ diff --git a/docs/images/favicon/apple-touch-icon.png b/docs/images/favicon/apple-touch-icon.png new file mode 100644 index 00000000000..8bc424ef7f4 Binary files /dev/null and b/docs/images/favicon/apple-touch-icon.png differ diff --git a/docs/images/favicon/browserconfig.xml b/docs/images/favicon/browserconfig.xml new file mode 100644 index 00000000000..50175558c4f --- /dev/null +++ b/docs/images/favicon/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/docs/images/favicon/favicon-16x16.png b/docs/images/favicon/favicon-16x16.png new file mode 100644 index 00000000000..bb4f138334b Binary files /dev/null and b/docs/images/favicon/favicon-16x16.png differ diff --git a/docs/images/favicon/favicon-32x32.png b/docs/images/favicon/favicon-32x32.png new file mode 100644 index 00000000000..aebe620e20e Binary files /dev/null and b/docs/images/favicon/favicon-32x32.png differ diff --git a/docs/images/favicon/mstile-150x150.png b/docs/images/favicon/mstile-150x150.png new file mode 100644 index 00000000000..42e274f9147 Binary files /dev/null and b/docs/images/favicon/mstile-150x150.png differ diff --git a/docs/images/favicon/safari-pinned-tab.svg b/docs/images/favicon/safari-pinned-tab.svg new file mode 100644 index 00000000000..ddfdd8dfda4 --- /dev/null +++ b/docs/images/favicon/safari-pinned-tab.svg @@ -0,0 +1,109 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/docs/images/favicon/site.webmanifest b/docs/images/favicon/site.webmanifest new file mode 100644 index 00000000000..b685b7c82b0 --- /dev/null +++ b/docs/images/favicon/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/docs/images/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/docs/images/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png deleted file mode 100644 index 235da1c273e..00000000000 Binary files a/docs/images/findAlexDavidResult.png and /dev/null differ diff --git a/docs/images/findFriendsResult.png b/docs/images/findFriendsResult.png new file mode 100644 index 00000000000..6610e04e9e7 Binary files /dev/null and b/docs/images/findFriendsResult.png differ diff --git a/docs/images/ken-lai.png b/docs/images/ken-lai.png new file mode 100644 index 00000000000..f6152cfc48f Binary files /dev/null and b/docs/images/ken-lai.png differ diff --git a/docs/images/kurtyjlee.png b/docs/images/kurtyjlee.png new file mode 100644 index 00000000000..ce6eead4669 Binary files /dev/null and b/docs/images/kurtyjlee.png differ diff --git a/docs/images/leezhengjing.png b/docs/images/leezhengjing.png new file mode 100644 index 00000000000..5bdf3124fdf Binary files /dev/null and b/docs/images/leezhengjing.png differ diff --git a/docs/images/nicleongyj.png b/docs/images/nicleongyj.png new file mode 100644 index 00000000000..95d79f6d3f3 Binary files /dev/null and b/docs/images/nicleongyj.png differ diff --git a/docs/images/tutorialAdd.png b/docs/images/tutorialAdd.png new file mode 100644 index 00000000000..dbe27de0f0e Binary files /dev/null and b/docs/images/tutorialAdd.png differ diff --git a/docs/images/tutorialAddMore.png b/docs/images/tutorialAddMore.png new file mode 100644 index 00000000000..ccbe7a407b1 Binary files /dev/null and b/docs/images/tutorialAddMore.png differ diff --git a/docs/images/tutorialEdit.png b/docs/images/tutorialEdit.png new file mode 100644 index 00000000000..b4a05ecacfd Binary files /dev/null and b/docs/images/tutorialEdit.png differ diff --git a/docs/images/tutorialEventAdd.png b/docs/images/tutorialEventAdd.png new file mode 100644 index 00000000000..8f9725a643c Binary files /dev/null and b/docs/images/tutorialEventAdd.png differ diff --git a/docs/images/tutorialEventEdit.png b/docs/images/tutorialEventEdit.png new file mode 100644 index 00000000000..959a88578f3 Binary files /dev/null and b/docs/images/tutorialEventEdit.png differ diff --git a/docs/images/userInterfaceTutorial.png b/docs/images/userInterfaceTutorial.png new file mode 100644 index 00000000000..32d14d34905 Binary files /dev/null and b/docs/images/userInterfaceTutorial.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..5806c5aae34 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,17 @@ --- layout: page -title: AddressBook Level-3 +title: FumbleLog --- -[![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/AY2324S1-CS2103T-T12-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S1-CS2103T-T12-2/tp/actions) +[![codecov](https://codecov.io/gh/AY2324S1-CS2103T-T12-2/tp/graph/badge.svg?token=MDL2TF28EC)](https://codecov.io/gh/AY2324S1-CS2103T-T12-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). +**FumbleLog is a productivity companion for managing your friendships, meetings, tasks and life.** -* 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 FumbleLog, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing FumbleLog, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/docs/team/distractedcat.md b/docs/team/distractedcat.md new file mode 100644 index 00000000000..be8bcf9b819 --- /dev/null +++ b/docs/team/distractedcat.md @@ -0,0 +1,58 @@ +--- +layout: page +title: Guo Yuheng's Project Portfolio Page +--- + +### Project: Fumblelog + +FumbleLog is a desktop productivity application used for managing contacts and events. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. +**Given below are my contributions to the project:** + +* **Code contributed**: [DistractedCat](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=distractedcat&breakdown=true) +## Contributions to the project + +### Enhancements Implemented +* Added new command to add events +* Added ability to assign persons to events +* Added ability to unassign persons from events +* Added ability for events to be updated when there is a change in the assigned persons +* Update the `add_person` command to appropriately handle assigning and un-assigning of groups and persons. +* Update the `edit_person` command to appropriately handle assigning and un-assigning of groups and persons. +* Update the `delete_person` command to appropriately handle unassigning of persons +* Added utility methods for commands +* Added ability to check for valid dates +* Added UI view for expired events by displaying them in a different card +* Added test cases for event commands, parsers and model. +* Added test utils for MeetingBuilder, TypicalMeetings and TypicalPersons. + +### Contributions to the Developer Guide (DG) + +* Added use cases for UC01-UC08 +* Added activity diagram and object diagram +* Update details on program flow + +### Contributions to the User Guide (UG) +* Added documentation for + * `edit_event` command, where the user can edit the details of an event. + * `delete_event` command, where the user can delete an event. + * Assigning, Un-assigning persons, where users can do through the `add_event` and `edit_event` commands. +* Updated documentation for + * `add_event` commands, where I detailed how to add events with persons, and to unassign them from the event + * Time requirements and behaviors when users are creating an event, since the user can add events without start or end times. + +* Provided cosmetic enhancements and updates to the user guide. +* Updated images for the user guide. +* Updated commands with clearer descriptions of FumbleLog behaviour. + + +### Contributions to team-based tasks +* Conducted code reviews for PRs. +* Provided feedback for PRs. +* Helped increase code coverage by adding test cases. +* Created issues to track bugs and enhancements. +* Helped with the documentation process. +* Coordinated sprints and code review. +* Fixed bugs and issues. + +### Contributions beyond the project team +* Good moral support. diff --git a/docs/team/ken-lai.md b/docs/team/ken-lai.md new file mode 100644 index 00000000000..171b415903a --- /dev/null +++ b/docs/team/ken-lai.md @@ -0,0 +1,45 @@ +--- +layout: page +title: Lai Wei Zhong's Project Portfolio Page +--- + +# Project: FumbleLog + +FumbleLog is a desktop productivity application used for managing contacts and events. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +## Contributions to the project + +### Code contribution +* [Link to RepoSense](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=ken-lai&breakdown=false&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos) + +### Enhancements Implemented +* Added new command to find persons. +* Added new command to find events. +* Enhanced the GUI to display events. +* Enhanced the `find` command to find all persons and events. +* Added test cases to find commands, parsers and models. +* Added test utils for AddressBookBuilder, TypicalMeetings and TypicalPersons. + +### Contributions to the About Us +* Added group members' pictures, github and portfolio links. + +### Contributions to the Developer Guide (DG) +* Added implementation details for finding persons and events. +* Added class diagrams and sequence diagrams. + +### Contributions to the User Guide (UG) +* Updated documentation for + * Command notes, where I detailed how FumbleLog handles commands with invalid prefixes. + * `add_person` and `edit_person`, where I detailed the constraints for phone number. +* Fixed typos and bugs in the user guide. + +### Contributions to team-based tasks +* Reviewed and provided feedback for PRs: [\#18](https://github.com/AY2324S1-CS2103T-T12-2/tp/pull/18), [\#35](https://github.com/AY2324S1-CS2103T-T12-2/tp/pull/35), [\#38](https://github.com/AY2324S1-CS2103T-T12-2/tp/pull/38), [\#57](https://github.com/AY2324S1-CS2103T-T12-2/tp/pull/57), [\#76](https://github.com/AY2324S1-CS2103T-T12-2/tp/pull/76), [\#129](https://github.com/AY2324S1-CS2103T-T12-2/tp/pull/129), [\#130](https://github.com/AY2324S1-CS2103T-T12-2/tp/pull/130), [\#143](https://github.com/AY2324S1-CS2103T-T12-2/tp/pull/143), [\#147](https://github.com/AY2324S1-CS2103T-T12-2/tp/pull/147) +* Helped increase code coverage by adding test cases. +* Created issues to track bugs and enhancements. +* Helped with the documentation process. +* Fixed bugs and issues: [\#78](https://github.com/AY2324S1-CS2103T-T12-2/tp/issues/78), [\#88](https://github.com/AY2324S1-CS2103T-T12-2/tp/issues/88), [\#93](https://github.com/AY2324S1-CS2103T-T12-2/tp/issues/93), [\#98](https://github.com/AY2324S1-CS2103T-T12-2/tp/issues/98), [\#104](https://github.com/AY2324S1-CS2103T-T12-2/tp/issues/104), [\#109](https://github.com/AY2324S1-CS2103T-T12-2/tp/issues/109), [\#115](https://github.com/AY2324S1-CS2103T-T12-2/tp/issues/115), [\#122](https://github.com/AY2324S1-CS2103T-T12-2/tp/issues/122) + +### Contributions beyond the project team +* Good moral support. +* Provide structural opinions and suggestions for improvements. diff --git a/docs/team/kurtyjlee.md b/docs/team/kurtyjlee.md new file mode 100644 index 00000000000..88d715a2c3e --- /dev/null +++ b/docs/team/kurtyjlee.md @@ -0,0 +1,61 @@ +--- +layout: page +title: Kurt Lee's Project Portfolio Page +--- + +# Project: FumbleLog + +FumbleLog is a desktop productivity application used for managing contacts and events. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +## Contributions to the project + +### Code contribution +* [Link to RepoSense](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=kurtyjlee&breakdown=false&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos) + +### Enhancements Implemented +* Added new command to edit events. +* Added new command to delete events. +* Added new command to assign groups to events. +* Added new command to unassign groups from events. +* Updated the `delete` command delete empty groups from events. +* Updated the `add` command to appropriately handle assigning and un-assigning of groups and persons. +* Updated the `edit` command to appropriately handle assigning and un-assigning of groups and persons. +* Added test cases for event commands, parsers and models. +* Added test utils for MeetingBuilder, TypicalMeetings and TypicalPersons. + +### Contributions to the Developer Guide (DG) +* Added implementation details for tracking Events. +* Updated use cases for + * Events CRUD operations + * Assigning groups + * Unassigning groups + * Find by groups. +* Added class diagrams and sequence diagrams. + +### Contributions to the User Guide (UG) +* Added documentation for + * `edit_event` command, where the user can edit the details of an event. + * `delete_event` command, where the user can delete an event. + * Assigning, Un-assigning Groups, where users can do through the `add_event` and `edit_event` commands. +* Updated documentation for + * `add_event` commands, where I detailed how to add events with groups. + * `add` command, where I detailed how to add persons with groups. + * `edit` command, where I detailed how to edit persons with groups. + * `delete` command, where I detailed how to delete persons with groups. +* Provided cosmetic enhancements and updates to the user guide. +* Updated images for the user guide. +* Updated commands with clearer descriptions of FumbleLog behaviour. +* Added FumbleLogs Favicon to the website + +### Contributions to team-based tasks +* Conducted code reviews for PRs. +* Provided feedback for PRs. +* Helped increase code coverage by adding test cases. +* Created issues to track bugs and enhancements. +* Helped with the release process. +* Helped with the documentation process. +* Coordinated sprints and code review. +* Fixed bugs and issues. + +### Contributions beyond the project team +* Good moral support. diff --git a/docs/team/leezhengjing.md b/docs/team/leezhengjing.md new file mode 100644 index 00000000000..465b1bf0469 --- /dev/null +++ b/docs/team/leezhengjing.md @@ -0,0 +1,54 @@ +--- +layout: page +title: Lee Zheng Jing's Project Portfolio Page +--- + +# Project: Fumblelog + +FumbleLog is a desktop productivity application used for managing contacts and events. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +## Contributions to the project + +### Code contribution +* [leezhengjing](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=leezhengjing&breakdown=true) + +### Enhancements Implemented +* Added new command to list all events. +* Added new command to list all persons +* Added new command to list all persons and events. +* Update the `add_person` command to appropriately handle blank fields by utilising Optionals. +* Update the `add_person` command to include a new field, Remarks. +* Added test cases for core components, util, commands, parsers and model. + +### Contributions to the Developer Guide (DG) +* Updated implementation details for + * add_person command + * list_all command + * list_person command + * list_event command +* Added class diagrams and sequence diagrams, for parser commands, add_person and delete_person command. + +### Contributions to the User Guide (UG) +* Added documentation for + * `add_person` command, where the user can add a person into FumbleLog + * `list_all` command, where the user can list all persons and events. + * `list_person` command, where the user can list all persons. + * `list_event` command, where the user can list all events. +* Updated documentation for + * `add_person` commands, where I detailed how to add a person into FumbleLog +* Provided cosmetic enhancements and updates to the user guide. +* Updated images for the user guide. + + +### Contributions to team-based tasks +* Conducted code reviews for PRs. +* Provided feedback for PRs. +* Helped increase code coverage by adding test cases. +* Created issues to track bugs and enhancements. +* Helped with the release process. +* Helped with the documentation process. +* Coordinated sprints and code review. +* Fixed bugs and issues. + +### Contributions beyond the project team +* Good moral support. diff --git a/docs/team/nicleongyj.md b/docs/team/nicleongyj.md new file mode 100644 index 00000000000..4b70846127e --- /dev/null +++ b/docs/team/nicleongyj.md @@ -0,0 +1,54 @@ +--- +layout: page +title: Leong Yu Jun Nicholas's Project Portfolio Page +--- + +### Project: Fumblelog + +FumbleLog is a desktop productivity application used for managing contacts and events. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +## Contributions to the project + +### Code contribution +* [Link to RepoSense](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=nicleongyj&breakdown=true) + +### Enhancements Implemented +* Added `Birthday` field to `Person` class. +* Updated `add` and `edit` command to accept birthday argument. +* Added new `remind` command to display all events and birthdays in upcoming week. +* Updated `remind` to accept optional arguments to specify number of days to look ahead. +* Added predicate classes to filter events and birthdays by date. +* Added test cases for birthday, remind, predicates, and parser. + +### Contributions to the Developer Guide (DG) +* Added implementation details for remind command. +* Added sequence diagram for remind command. +* Updated project target user profile and value proposition. +* Updated project user stories. + +### Contributions to the User Guide (UG) +* Added documentation for + * `remind` command, where users can be reminded of upcoming events and birthdays. +* Added table to navigate users of different experiences to relevant parts of the user guide. +* Updated Quick Start section to include detailed information targeted at newer users. +* Added a orientation guide to the GUI of FumbleLog. +* Added a step-by-step tutorial to orientate new users through FumbleLog commands. +* Updated each FumbleLog command to include parameter tables. +* Provided cosmetic enhancements to the user guide. +* Added images for the user guide. + + +### Contributions to team-based tasks +* Conducted code reviews for PRs. +* Provided feedback for PRs. +* Helped increase code coverage by adding test cases. +* Created issues to track bugs and enhancements. +* Helped with the documentation process. +* Coordinated sprints and code review. +* Fixed bugs and issues. + +### Contributions beyond the project team +* Good moral support. + + + diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md index 4fb62a83ef6..9cdc61228e5 100644 --- a/docs/tutorials/TracingCode.md +++ b/docs/tutorials/TracingCode.md @@ -85,7 +85,7 @@ Now let’s set the breakpoint. First, double-click the item to reach the corres ## Tracing the execution path -Recall from the User Guide that the `edit` command has the format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` For this tutorial we will be issuing the command `edit 1 n/Alice Yeoh`. +Recall from the User Guide that the `edit_person` command has the format: `edit_person PERSON_INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [b/BIRTHDAY] [r/REMARK] [g/GROUP]…​ [ug/GROUP]…​` For this tutorial we will be issuing the command `edit_person 1 n/Alice Yeoh`.
@@ -94,7 +94,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. To start the debugging session, simply `Run` \> `Debug Main` -1. When the GUI appears, enter `edit 1 n/Alice Yeoh` into the command box and press `Enter`. +1. When the GUI appears, enter `edit_person 1 n/Alice Yeoh` into the command box and press `Enter`. 1. The Debugger tool window should show up and show something like this:
![DebuggerStep1](../images/tracing/DebuggerStep1.png) @@ -159,12 +159,12 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ ``` java ... - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); + case EditPersonCommand.COMMAND_WORD: + return new EditPersonCommandParser().parse(arguments); ... ``` -1. Let’s see what `EditCommandParser#parse()` does by stepping into it. You might have to click the 'step into' button multiple times here because there are two method calls in that statement: `EditCommandParser()` and `parse()`. +1. Let’s see what `EditPersonCommandParser#parse()` does by stepping into it. You might have to click the 'step into' button multiple times here because there are two method calls in that statement: `EditCommandParser()` and `parse()`.
:bulb: **Intellij Tip:** Sometimes, you might end up stepping into functions that are not of interest. Simply use the `step out` button to get out of them!
@@ -184,31 +184,50 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ 1. Now, step over until you read the statement that calls the `execute()` method of the `EditCommand` object received, and step into that `execute()` method (partial code given below): - **`EditCommand#execute()`:** + **`EditPersonCommand#execute()`:** ``` java @Override public CommandResult execute(Model model) throws CommandException { ... Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); + Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + + // This check must happen first to check duplicate persons + if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + System.out.println("duplicate detected 2"); + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } + + model.setPerson(personToEdit, editedPerson); + + // Update assigned persons in the event list + model.updateAssignedPersons(personToEdit, editedPerson); + + /* Remove empty groups from event when un-assigning groups from persons + and that person is the last member of the group + */ + Set emptyGroups = model.getEmptyGroups(personToEdit); + if (!emptyGroups.isEmpty()) { + for (Group group : emptyGroups) { + logger.info(String.format("Removing empty group: %s", group)); + } + model.removeEmptyGroups(emptyGroups); + } else { + model.updateGroups(); + } + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); } ``` 1. As suspected, `command#execute()` does indeed make changes to the `model` object. Specifically, * it uses the `setPerson()` method (defined in the interface `Model` and implemented in `ModelManager` as per the usual pattern) to update the person data. * it uses the `updateFilteredPersonList` method to ask the `Model` to populate the 'filtered list' with _all_ persons.
- FYI, The 'filtered list' is the list of persons resulting from the most recent operation that will be shown to the user immediately after. For the `edit` command, we populate it with all the persons so that the user can see the edited person along with all other persons. If this was a `find` command, we would be setting that list to contain the search results instead.
+ FYI, The 'filtered list' is the list of persons resulting from the most recent operation that will be shown to the user immediately after. For the `edit_person` command, we populate it with all the persons so that the user can see the edited person along with all other persons. If this was a `find` command, we would be setting that list to contain the search results instead.
To provide some context, given below is the class diagram of the `Model` component. See if you can figure out where the 'filtered list' of persons is being tracked.
* :bulb: This may be a good time to read through the [`Model` component section of the DG](../DeveloperGuide.html#model-component) -1. As you step through the rest of the statements in the `EditCommand#execute()` method, you'll see that it creates a `CommandResult` object (containing information about the result of the execution) and returns it.
+1. As you step through the rest of the statements in the `EditPersonCommand#execute()` method, you'll see that it creates a `CommandResult` object (containing information about the result of the execution) and returns it.
Advancing the debugger by one more step should take you back to the middle of the `LogicManager#execute()` method.
1. Given that you have already seen quite a few classes in the `Logic` component in action, see if you can identify in this partial class diagram some of the classes you've encountered so far, and see how they fit into the class structure of the `Logic` component: @@ -273,24 +292,24 @@ Here are some quick questions you can try to answer based on your execution path instead? What exceptions do you think will be thrown (if any), where will the exceptions be thrown and where will they be handled? - 1. `redit 1 n/Alice Yu` + 1. `edit_person 1 n/Alice Yu` - 2. `edit 0 n/Alice Yu` + 2. `edit_person 0 n/Alice Yu` - 3. `edit 1 n/Alex Yeoh` + 3. `edit_person 1 n/Alex Yeoh` - 4. `edit 1` + 4. `edit_person 1` - 5. `edit 1 n/アリス ユー` + 5. `edit_person 1 n/アリス ユー` - 6. `edit 1 t/one t/two t/three t/one` + 6. `edit_person 1 t/one t/two t/three t/one` 2. What components will you have to modify to perform the following enhancements to the application? 1. Make command words case-insensitive - 2. Allow `delete` to remove more than one index at a time + 2. Allow `delete_person` to remove more than one index at a time 3. Save the address book in the CSV format instead diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..d0e9b7071d5 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -84,6 +84,7 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { + " populated with a sample AddressBook."); } initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + } catch (DataLoadingException e) { logger.warning("Data file at " + storage.getAddressBookFilePath() + " could not be loaded." + " Will be starting with an empty AddressBook."); diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/address/commons/core/LogsCenter.java index 8cf8e15a0f0..60bf6145139 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/address/commons/core/LogsCenter.java @@ -102,5 +102,4 @@ private static void setBaseLogger() { } } - } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..cc77cbcd2e0 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.event.Event; import seedu.address.model.person.Person; /** @@ -33,6 +34,12 @@ public interface Logic { /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the full list of persons */ + ObservableList getFullPersonList(); + + /** Returns an unmodifiable view of events */ + ObservableList getFilteredEventList(); + /** * Returns the user prefs' address book file path. */ diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..c190b726121 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -15,6 +15,7 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.event.Event; import seedu.address.model.person.Person; import seedu.address.storage.Storage; @@ -57,7 +58,6 @@ public CommandResult execute(String commandText) throws CommandException, ParseE } catch (IOException ioe) { throw new CommandException(String.format(FILE_OPS_ERROR_FORMAT, ioe.getMessage()), ioe); } - return commandResult; } @@ -71,6 +71,16 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getFullPersonList() { + return model.getFullPersonList(); + } + + @Override + public ObservableList getFilteredEventList() { + return model.getFilteredEventList(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..c2ec8466222 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -5,6 +5,9 @@ import java.util.stream.Stream; import seedu.address.logic.parser.Prefix; +import seedu.address.model.event.Event; +import seedu.address.model.group.Group; +import seedu.address.model.person.Name; import seedu.address.model.person.Person; /** @@ -14,10 +17,32 @@ 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"; + + /** Message for person */ public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; + public static final String MESSAGE_PERSONS_AND_EVENTS_LISTED_OVERVIEW = "%d persons and %d events listed!"; + public static final String MESSAGE_PERSON_AND_EVENTS_LISTED_OVERVIEW = "%d person and %d events listed!"; + public static final String MESSAGE_PERSONS_AND_EVENT_LISTED_OVERVIEW = "%d persons and %d event listed!"; + public static final String MESSAGE_PERSON_AND_EVENT_LISTED_OVERVIEW = "%d person and %d event listed!"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_PERSON_LISTED_OVERVIEW = "%1$d person listed!"; + public static final String MESSAGE_EVENTS_LISTED_OVERVIEW = "%1$d events listed!"; + public static final String MESSAGE_EVENT_LISTED_OVERVIEW = "%1$d event listed!"; + + public static final String MESSAGE_PERSONS_AND_EVENTS_SHOWN_OVERVIEW = + "Showing all birthdays and events happening in the next %1$s days:"; public static final String MESSAGE_DUPLICATE_FIELDS = - "Multiple values specified for the following single-valued field(s): "; + "Multiple values specified for the following single-valued field(s): "; + + /** Message for events */ + public static final String MESSAGE_INVALID_EVENT_DISPLAYED_INDEX = "The event index provided is invalid"; + public static final String MESSAGE_INVALID_PERSON = "The person(s) provided(%s) do not exist!"; + public static final String MESSAGE_INVALID_BIRTHDAY = "The person's birthday(%s) is invalid!"; + public static final String MESSAGE_INVALID_UNASSIGN_PERSON = + "The person(s) provided(%s) have not been assigned to the event!"; + public static final String MESSAGE_INVALID_GROUP = "The group(s) provided(%s) do not exist!"; + public static final String MESSAGE_INVALID_UNASSIGN_GROUP = + "The group(s) provided(%s) have not been assigned to the event!"; /** * Returns an error message indicating the duplicate prefixes. @@ -36,16 +61,66 @@ public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePref */ public static String format(Person person) { final StringBuilder builder = new StringBuilder(); - builder.append(person.getName()) - .append("; Phone: ") - .append(person.getPhone()) - .append("; Email: ") - .append(person.getEmail()) - .append("; Address: ") - .append(person.getAddress()) - .append("; Tags: "); - person.getTags().forEach(builder::append); + builder.append(person.getName()); + if (person.hasPhone()) { + builder.append("; Phone: ") + .append(person.getPhone()); + } + if (person.hasEmail()) { + builder.append("; Email: ") + .append(person.getEmail()); + } + if (person.hasAddress()) { + builder.append("; Address: ") + .append(person.getAddress()); + } + if (person.hasBirthday()) { + builder.append("; Birthday: ") + .append(person.getBirthday().forDisplay()); + } + if (person.hasRemark()) { + builder.append("; Remark: ") + .append(person.getRemark()); + } + if (person.hasGroups()) { + builder.append("; Groups: "); + person.getGroups().forEach(builder::append); + } return builder.toString(); } + /** + * Formats the {@code event} for display to the user. + */ + public static String formatEvent(Event event) { + final StringBuilder builder = new StringBuilder(); + builder.append(event.getName()) + .append("; Date: ") + .append(event.getStartDate().forDisplay()); + if (event.hasStartTime()) { + builder.append("; Start Time: ") + .append(event.getStartTime().forDisplay()); + } + if (event.hasEndTime()) { + builder.append("; End Time: ") + .append(event.getEndTime().forDisplay()); + } + if (!event.getNames().isEmpty()) { + builder.append("; Persons involved: "); + for (Name name : event.getNames()) { + builder.append(name.toString()); + builder.append(", "); + } + builder.delete(builder.length() - 2, builder.length()); //removes the last comma + } + if (!event.getGroups().isEmpty()) { + builder.append("; Groups involved: "); + for (Group group : event.getGroups()) { + builder.append(group.toString()); + builder.append(", "); + } + builder.delete(builder.length() - 2, builder.length()); //removes the last comma + } + return builder.toString(); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddEventCommand.java b/src/main/java/seedu/address/logic/commands/AddEventCommand.java new file mode 100644 index 00000000000..5124ae9e8d5 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddEventCommand.java @@ -0,0 +1,115 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; + +import java.util.Set; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Meeting; +import seedu.address.model.group.Group; +import seedu.address.model.person.Name; + + +/** + * Adds an event. + * @author Yuheng + */ +public class AddEventCommand extends Command { + + public static final String COMMAND_WORD = "add_event"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an event to the address book. \n" + + "Parameters: " + + PREFIX_EVENT_NAME + "EVENT_NAME " + + PREFIX_DATE + "DATE " + + "[" + PREFIX_START_TIME + "START_TIME] " + + "[" + PREFIX_END_TIME + "END_TIME] " + + "[" + PREFIX_NAME + "NAME]... " + + "[" + PREFIX_GROUP + "GROUP]... \n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_EVENT_NAME + "FumbleLog Meeting " + + PREFIX_DATE + "2024-01-30 " + + PREFIX_START_TIME + "1000 " + + PREFIX_END_TIME + "1200 " + + PREFIX_NAME + "Ken " + + PREFIX_NAME + "Yuheng " + + PREFIX_GROUP + "Team2 "; + + public static final String MESSAGE_SUCCESS = "New event added: %1$s"; + private final Meeting toAdd; + + /** + * Creates an AddEventCommand to add the specified {@code Meeting} + */ + public AddEventCommand(Meeting meeting) { + requireAllNonNull(meeting); + this.toAdd = meeting; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + CommandUtil.verifyEventTimes(this.toAdd); + + Set invalidNames = model.findInvalidNames(this.toAdd.getNames()); + + if (!invalidNames.isEmpty()) { + throw new CommandException(String.format(Messages.MESSAGE_INVALID_PERSON, + listInvalidNames(invalidNames))); + } + + Set invalidGroups = model.findInvalidGroups(this.toAdd.getGroups()); + + if (!invalidGroups.isEmpty()) { + throw new CommandException(String.format(Messages.MESSAGE_INVALID_GROUP, + listInvalidGroups(invalidGroups))); + } + + // This line must be at the end + assert invalidNames.isEmpty() || invalidGroups.isEmpty() : "Invalid names and groups should be checked first"; + model.addEvent(this.toAdd); + + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.formatEvent(toAdd))); + } + + private String listInvalidNames(Set invalidNames) { + StringBuilder builder = new StringBuilder(); + for (Name name : invalidNames) { + builder.append(name.toString()); + builder.append(", "); + } + builder.delete(builder.length() - 2, builder.length()); //removes the last comma + return builder.toString(); + } + + private String listInvalidGroups(Set invalidGroups) { + StringBuilder builder = new StringBuilder(); + for (Group group : invalidGroups) { + builder.append(group.toString()); + builder.append(", "); + } + builder.delete(builder.length() - 2, builder.length()); //removes the last comma + return builder.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (!(other instanceof AddEventCommand)) { + return false; + } else { + AddEventCommand otherAddEventCommand = (AddEventCommand) other; + return this.toAdd.equals(otherAddEventCommand.toAdd); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddPersonCommand.java similarity index 65% rename from src/main/java/seedu/address/logic/commands/AddCommand.java rename to src/main/java/seedu/address/logic/commands/AddPersonCommand.java index 5d7185a9680..a4bd501404c 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddPersonCommand.java @@ -1,11 +1,16 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_BIRTHDAY; 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_GROUP; 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 static seedu.address.logic.parser.CliSyntax.PREFIX_REMARK; + +import java.time.LocalDate; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; @@ -16,9 +21,9 @@ /** * Adds a person to the address book. */ -public class AddCommand extends Command { +public class AddPersonCommand extends Command { - public static final String COMMAND_WORD = "add"; + public static final String COMMAND_WORD = "add_person"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + "Parameters: " @@ -26,14 +31,17 @@ public class AddCommand extends Command { + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" + + PREFIX_BIRTHDAY + "BIRTHDAY " + + "[" + PREFIX_GROUP + "GROUP]...\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 + "2001-12-31 " + + PREFIX_REMARK + "Likes to swim " + + PREFIX_GROUP + "friends " + + PREFIX_GROUP + "owesMoney"; 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"; @@ -43,7 +51,7 @@ public class AddCommand extends Command { /** * Creates an AddCommand to add the specified {@code Person} */ - public AddCommand(Person person) { + public AddPersonCommand(Person person) { requireNonNull(person); toAdd = person; } @@ -56,7 +64,13 @@ public CommandResult execute(Model model) throws CommandException { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } + LocalDate birthday = toAdd.getBirthday().getValue(); + if (birthday != null && birthday.isAfter(LocalDate.now())) { + throw new CommandException(String.format(MESSAGE_INVALID_BIRTHDAY, toAdd.getBirthday().getStringValue())); + } + model.addPerson(toAdd); + model.updateGroups(); return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); } @@ -67,12 +81,12 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof AddCommand)) { + if (!(other instanceof AddPersonCommand)) { return false; } - AddCommand otherAddCommand = (AddCommand) other; - return toAdd.equals(otherAddCommand.toAdd); + AddPersonCommand otherAddPersonCommand = (AddPersonCommand) other; + return toAdd.equals(otherAddPersonCommand.toAdd); } @Override diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..e62ad0df2d5 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -13,7 +13,6 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - @Override public CommandResult execute(Model model) { requireNonNull(model); diff --git a/src/main/java/seedu/address/logic/commands/CommandUtil.java b/src/main/java/seedu/address/logic/commands/CommandUtil.java new file mode 100644 index 00000000000..b1f2e8481c7 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CommandUtil.java @@ -0,0 +1,57 @@ +package seedu.address.logic.commands; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.Event; + +/** + * Represents utility tools used during command execution. + */ +public class CommandUtil { + + /** + * Verifies that 1) Start time is not earlier than current time, + * 2) End Time is not earlier start time. + * @param event Event to be verified. + * + * @throws ParseException if the given {@code time} does not meet any of the above 2 criteria. + */ + public static void verifyEventTimes(Event event) throws CommandException { + + if (!event.hasStartTime() && !event.hasEndTime()) { + verifyTimeIsAfterCurrentTime(event.getStartDate().getDate().atTime(LocalTime.MAX)); + } else if (!event.hasStartTime()) { + verifyTimeIsAfterCurrentTime(event.getEndDate().getDate() + .atTime(event.getEndTime().getEventTime())); + } else if (!event.hasEndTime()) { + verifyTimeIsAfterCurrentTime(event.getStartDate().getDate() + .atTime(event.getStartTime().getEventTime())); + } else { + verifyTimeIsAfterCurrentTime(event.getStartDate().getDate() + .atTime(event.getStartTime().getEventTime())); + verifyEndTimeIsAfterStartTime(event); + } + + } + + private static void verifyEndTimeIsAfterStartTime(Event event) throws CommandException { + LocalDateTime combinedStartTime = event.getStartDate().getDate() + .atTime(event.getStartTime().getEventTime()); + + LocalDateTime combinedEndTime = event.getEndDate().getDate() + .atTime(event.getEndTime().getEventTime()); + + if (combinedEndTime.isBefore(combinedStartTime)) { + throw new CommandException(Event.END_TIME_CONSTRAINTS); + } + } + + private static void verifyTimeIsAfterCurrentTime(LocalDateTime time) throws CommandException { + if (time.isBefore(LocalDateTime.now())) { + throw new CommandException(Event.START_TIME_CONSTRAINTS); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteEventCommand.java b/src/main/java/seedu/address/logic/commands/DeleteEventCommand.java new file mode 100644 index 00000000000..9574ceafaf6 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteEventCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; + +/** + * Deletes a meeting identified using it's displayed index from the address book. + */ +public class DeleteEventCommand extends Command { + + public static final String COMMAND_WORD = "delete_event"; + + public static final String MESSAGE_USAGE = + COMMAND_WORD + ": Deletes the event identified by the index number used in the displayed event list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_EVENT_SUCCESS = "Deleted Event: %1$s"; + + private final Index targetIndex; + + /** + * Creates an DeleteMeetingCommand to delete the specified {@code Meeting} + * @param targetIndex index of the meeting in the filtered meeting list to delete + */ + public DeleteEventCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredEventList(); + + if (this.targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + Event eventToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteEvent(eventToDelete); + + return new CommandResult(String.format(MESSAGE_DELETE_EVENT_SUCCESS, Messages.formatEvent(eventToDelete))); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteEventCommand // instanceof handles nulls + && targetIndex.equals(((DeleteEventCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeletePersonCommand.java similarity index 64% rename from src/main/java/seedu/address/logic/commands/DeleteCommand.java rename to src/main/java/seedu/address/logic/commands/DeletePersonCommand.java index 1135ac19b74..7dbdd051117 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeletePersonCommand.java @@ -3,20 +3,24 @@ import static java.util.Objects.requireNonNull; import java.util.List; +import java.util.Set; +import java.util.logging.Logger; +import seedu.address.commons.core.LogsCenter; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.group.Group; import seedu.address.model.person.Person; /** * Deletes a person identified using it's displayed index from the address book. */ -public class DeleteCommand extends Command { +public class DeletePersonCommand extends Command { - public static final String COMMAND_WORD = "delete"; + public static final String COMMAND_WORD = "delete_person"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes the person identified by the index number used in the displayed person list.\n" @@ -27,7 +31,9 @@ public class DeleteCommand extends Command { private final Index targetIndex; - public DeleteCommand(Index targetIndex) { + private Logger logger = LogsCenter.getLogger(DeletePersonCommand.class); + + public DeletePersonCommand(Index targetIndex) { this.targetIndex = targetIndex; } @@ -41,7 +47,20 @@ public CommandResult execute(Model model) throws CommandException { } Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.updateAssignedPersons(personToDelete); model.deletePerson(personToDelete); + + // Group operation + Set emptyGroups = model.getEmptyGroups(personToDelete); + if (!emptyGroups.isEmpty()) { + for (Group group : emptyGroups) { + logger.info(String.format("Removing empty group: %s", group)); + } + model.removeEmptyGroups(emptyGroups); + } else { + model.updateGroups(); + } + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); } @@ -52,12 +71,12 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof DeleteCommand)) { + if (!(other instanceof DeletePersonCommand)) { return false; } - DeleteCommand otherDeleteCommand = (DeleteCommand) other; - return targetIndex.equals(otherDeleteCommand.targetIndex); + DeletePersonCommand otherDeletePersonCommand = (DeletePersonCommand) other; + return targetIndex.equals(otherDeletePersonCommand.targetIndex); } @Override diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java deleted file mode 100644 index 4b581c7331e..00000000000 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ /dev/null @@ -1,242 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; -import seedu.address.commons.util.ToStringBuilder; -import seedu.address.logic.Messages; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Edits the details of an existing person in the address book. - */ -public class EditCommand extends Command { - - public static final String COMMAND_WORD = "edit"; - - 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) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; - - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; - public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - - private final Index index; - private final EditPersonDescriptor editPersonDescriptor; - - /** - * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with - */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { - requireNonNull(index); - requireNonNull(editPersonDescriptor); - - this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); - } - - @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 = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); - } - - /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. - */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; - - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditCommand)) { - return false; - } - - EditCommand otherEditCommand = (EditCommand) other; - return index.equals(otherEditCommand.index) - && editPersonDescriptor.equals(otherEditCommand.editPersonDescriptor); - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .add("index", index) - .add("editPersonDescriptor", editPersonDescriptor) - .toString(); - } - - /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. - */ - public static class EditPersonDescriptor { - private Name name; - private Phone phone; - private Email email; - private Address address; - private Set tags; - - public EditPersonDescriptor() {} - - /** - * Copy constructor. - * A defensive copy of {@code tags} is used internally. - */ - public EditPersonDescriptor(EditPersonDescriptor toCopy) { - setName(toCopy.name); - setPhone(toCopy.phone); - setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); - } - - /** - * Returns true if at least one field is edited. - */ - public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); - } - - public void setName(Name name) { - this.name = name; - } - - public Optional getName() { - return Optional.ofNullable(name); - } - - public void setPhone(Phone phone) { - this.phone = phone; - } - - public Optional getPhone() { - return Optional.ofNullable(phone); - } - - public void setEmail(Email email) { - this.email = email; - } - - public Optional getEmail() { - return Optional.ofNullable(email); - } - - public void setAddress(Address address) { - this.address = address; - } - - public Optional
getAddress() { - return Optional.ofNullable(address); - } - - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; - } - - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { - return false; - } - - EditPersonDescriptor otherEditPersonDescriptor = (EditPersonDescriptor) other; - return Objects.equals(name, otherEditPersonDescriptor.name) - && Objects.equals(phone, otherEditPersonDescriptor.phone) - && Objects.equals(email, otherEditPersonDescriptor.email) - && Objects.equals(address, otherEditPersonDescriptor.address) - && Objects.equals(tags, otherEditPersonDescriptor.tags); - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .add("name", name) - .add("phone", phone) - .add("email", email) - .add("address", address) - .add("tags", tags) - .toString(); - } - } -} diff --git a/src/main/java/seedu/address/logic/commands/EditEventCommand.java b/src/main/java/seedu/address/logic/commands/EditEventCommand.java new file mode 100644 index 00000000000..85d61718df3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditEventCommand.java @@ -0,0 +1,397 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_UNASSIGN_GROUPS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_UNASSIGN_PERSONS; + +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.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDate; +import seedu.address.model.event.EventName; +import seedu.address.model.event.EventTime; +import seedu.address.model.event.Meeting; +import seedu.address.model.group.Group; +import seedu.address.model.person.Name; + + +/** + * Command to edit a meeting in the address book. + */ +public class EditEventCommand extends Command { + public static final String COMMAND_WORD = "edit_event"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the event identified " + + "by the index number used in the displayed event list.\n" + + "Existing values will be overwritten by the input values, except for " + + "the list of assigned persons and the list of assigned groups \n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_EVENT_NAME + "EVENT_DETAILS] " + + "[" + PREFIX_DATE + "DATE] " + + "[" + PREFIX_START_TIME + "START_TIME] " + + "[" + PREFIX_END_TIME + "END_TIME] " + + "[" + PREFIX_NAME + "NAME]... " + + "[" + PREFIX_UNASSIGN_PERSONS + "NAME]... " + + "[" + PREFIX_GROUP + "GROUP]... " + + "[" + PREFIX_UNASSIGN_GROUPS + "GROUP]... \n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_EVENT_NAME + "FumbleLog Meeting " + + PREFIX_DATE + "2023-10-13 " + + PREFIX_NAME + "Ken " + + PREFIX_GROUP + "Team2 "; + + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + + public static final String MESSAGE_EDIT_SUCCESS = "Edited event: %1$s"; + + public final Index index; + public final EditEventDescriptor editEventDescriptor; + + /** + * Takes in the index of the meeting to edit and its descriptor. + * @param index of the meeting to edit + * @param editEventDescriptor details to edit the meeting with + */ + public EditEventCommand(Index index, EditEventDescriptor editEventDescriptor) { + requireNonNull(index); + requireNonNull(editEventDescriptor); + this.index = index; + this.editEventDescriptor = editEventDescriptor; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getFilteredEventList(); + + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EVENT_DISPLAYED_INDEX); + } + + Event meetingToEdit = lastShownList.get(index.getZeroBased()); + Event editedMeeting = createEditedMeeting(meetingToEdit, this.editEventDescriptor, model); + + // ensure that the user is not editing a valid time into an invalid time + if (this.editEventDescriptor.getDate().isPresent() + || this.editEventDescriptor.getStartTime().isPresent() + || this.editEventDescriptor.getEndTime().isPresent()) { + CommandUtil.verifyEventTimes(editedMeeting); + } + + model.setEvent(meetingToEdit, editedMeeting); + + return new CommandResult(generateSuccessMessage(editedMeeting)); + } + + private static Event createEditedMeeting(Event meetingToEdit, + EditEventDescriptor editEventDescriptor, + Model model) throws CommandException { + assert meetingToEdit != null; + + // All attributes are optional, so if they are not present, use the original values + EventName updatedName = editEventDescriptor.getName().orElse(meetingToEdit.getName()); + EventDate updatedDate = editEventDescriptor.getDate().orElse(meetingToEdit.getStartDate()); + EventTime updatedStartTime = editEventDescriptor.getStartTime().orElse(meetingToEdit.getStartTime()); + EventTime updatedEndTime = editEventDescriptor.getEndTime().orElse(meetingToEdit.getEndTime()); + + // Edit persons + Set updatedPersonNames; + updatedPersonNames = handleEditAssignPersons(meetingToEdit, editEventDescriptor, model); + handleEditUnassignPersons(meetingToEdit, editEventDescriptor, model, updatedPersonNames); + + // Editing groups + Set updatedGroups; + updatedGroups = handleEditAssignGroups(meetingToEdit, editEventDescriptor, model); + handleEditUnassignGroups(meetingToEdit, editEventDescriptor); + + return new Meeting(updatedName, updatedDate, + Optional.of(updatedStartTime), Optional.of(updatedEndTime), updatedPersonNames, updatedGroups); + } + + private static void handleEditUnassignGroups(Event meetingToEdit, EditEventDescriptor editEventDescriptor) + throws CommandException { + if (editEventDescriptor.getUnassignGroups().isPresent()) { + if (!meetingToEdit.getGroups().containsAll(editEventDescriptor.getUnassignGroups().get())) { + + Set invalidUnassignGroups = findInvalidUnassignGroups(meetingToEdit, + editEventDescriptor.getUnassignGroups().get()); + + //case where the groups to be unassigned have not even been previously assigned + throw new CommandException(String.format(Messages.MESSAGE_INVALID_UNASSIGN_GROUP, + listInvalidGroups(invalidUnassignGroups))); + } + meetingToEdit.getGroups().removeAll(editEventDescriptor.getUnassignGroups().get()); + } + } + + private static Set handleEditAssignGroups(Event meetingToEdit, EditEventDescriptor editEventDescriptor, + Model model) throws CommandException { + Set updatedGroups; + if (editEventDescriptor.getGroups().isPresent()) { + Set invalidGroups = model.findInvalidGroups(editEventDescriptor.getGroups().get()); + if (!invalidGroups.isEmpty()) { + throw new CommandException(String.format(Messages.MESSAGE_INVALID_GROUP, + listInvalidGroups(invalidGroups))); + } + meetingToEdit.getGroups().addAll(editEventDescriptor.getGroups().get()); + } + updatedGroups = meetingToEdit.getGroups(); + return updatedGroups; + } + + private static void handleEditUnassignPersons(Event meetingToEdit, EditEventDescriptor editEventDescriptor, + Model model, Set updatedPersonNames) throws CommandException { + if (editEventDescriptor.getUnassignedPersons().isPresent()) { + Set invalidNames = model.findInvalidNames(editEventDescriptor.getUnassignedPersons().get()); + if (!invalidNames.isEmpty()) { + throw new CommandException(String.format(Messages.MESSAGE_INVALID_PERSON, + listInvalidNames(invalidNames))); + } else if (!meetingToEdit.getNames().containsAll(editEventDescriptor.getUnassignedPersons().get())) { + //case where the persons to be unassigned have not even been previously assigned + Set invalidUnassignNames = findInvalidUnassignNames(meetingToEdit, + editEventDescriptor.getUnassignedPersons().get()); + throw new CommandException(String.format(Messages.MESSAGE_INVALID_UNASSIGN_PERSON, + listInvalidNames(invalidUnassignNames))); + } + //remove the persons from the new list of persons + updatedPersonNames.removeAll(editEventDescriptor.getUnassignedPersons().get()); + } // no persons to be unassigned, do nothing + } + + private static Set handleEditAssignPersons(Event meetingToEdit, EditEventDescriptor editEventDescriptor, + Model model) throws CommandException { + Set updatedPersonNames; + if (editEventDescriptor.getAssignedPersons().isPresent()) { + Set invalidNames = model.findInvalidNames(editEventDescriptor.getAssignedPersons().get()); + if (!invalidNames.isEmpty()) { + throw new CommandException(String.format(Messages.MESSAGE_INVALID_PERSON, + listInvalidNames(invalidNames))); + } + //add the new persons to the existing list of persons + meetingToEdit.getNames().addAll(editEventDescriptor.getAssignedPersons().get()); + } + updatedPersonNames = meetingToEdit.getNames(); + return updatedPersonNames; + } + + private static Set findInvalidUnassignNames(Event meetingToEdit, Set unassignNames) { + Set invalidUnassignNames = new HashSet<>(); + for (Name name : unassignNames) { + if (!meetingToEdit.getNames().contains(name)) { + invalidUnassignNames.add(name); + } + } + return invalidUnassignNames; + } + + private static Set findInvalidUnassignGroups(Event meetingToEdit, Set unassignGroups) { + Set invalidUnassignGroups = new HashSet<>(); + for (Group group : unassignGroups) { + if (!meetingToEdit.getGroups().contains(group)) { + invalidUnassignGroups.add(group); + } + } + return invalidUnassignGroups; + } + + + /** + * generates a string of invalid names for display. + */ + private static String listInvalidNames(Set invalidNames) { + StringBuilder builder = new StringBuilder(); + for (Name name : invalidNames) { + builder.append(name.toString()); + builder.append(", "); + } + + builder.delete(builder.length() - 2, builder.length()); //removes the last comma + return builder.toString(); + } + + private static String listInvalidGroups(Set invalidGroups) { + StringBuilder builder = new StringBuilder(); + for (Group group : invalidGroups) { + builder.append(group.toString()); + builder.append(", "); + } + + builder.delete(builder.length() - 2, builder.length()); //removes the last comma + return builder.toString(); + } + + /** + * Generates a command execution success message with the meeting edited. + * @param meetingToEdit meeting that was edited + * @return String containing the success message + */ + private String generateSuccessMessage(Event meetingToEdit) { + return String.format(MESSAGE_EDIT_SUCCESS, Messages.formatEvent(meetingToEdit)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditEventCommand)) { + return false; + } + + EditEventCommand e = (EditEventCommand) other; + return index.equals(e.index) + && this.editEventDescriptor.equals(e.editEventDescriptor); + } + + /** + * Stores the details to edit the meeting with. Each non-empty field value will replace the + * corresponding field value of the meeting. + */ + public static class EditEventDescriptor { + + private EventName name; + private EventDate date; + private EventTime startTime; + private EventTime endTime; + + private Set assignPersons; + private Set unassignPersons; + + private Set assignGroups; + private Set unassignGroups; + + public EditEventDescriptor() { + } + + public Optional getName() { + return Optional.ofNullable(this.name); + } + + public Optional getDate() { + return Optional.ofNullable(this.date); + } + + public Optional getStartTime() { + return Optional.ofNullable(this.startTime); + } + + public Optional getEndTime() { + return Optional.ofNullable(this.endTime); + } + + public void setName(EventName name) { + this.name = name; + } + + public void setDate(EventDate date) { + this.date = date; + } + + public void setStartTime(EventTime startTime) { + this.startTime = startTime; + } + + public void setEndTime(EventTime endTime) { + this.endTime = endTime; + } + + /** + * Checks if fields are edited. + * @return true if at least one field is edited + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(this.name, this.date, this.startTime, + this.endTime, this.assignPersons, this.unassignPersons, this.assignGroups, this.unassignGroups); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", this.name) + .add("date", this.date.toString()) + .add("start time", this.startTime.toString()) + .add("end time", this.endTime.toString()) + .add("assign persons", this.assignPersons) + .add("unassign persons", this.unassignPersons) + .add("assign groups", this.assignGroups) + .add("unassign groups", this.unassignGroups) + .toString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditEventDescriptor)) { + return false; + } + + EditEventDescriptor e = (EditEventDescriptor) other; + return this.name.equals(e.name) + && this.date.equals(e.date) + && this.startTime.equals(e.startTime) + && this.endTime.equals(e.endTime); + } + + public void setPersonNames(Set names) { + this.assignPersons = (names != null) ? new HashSet<>(names) : null; + } + + public void setUnassignPersons(Set names) { + this.unassignPersons = (names != null) ? new HashSet<>(names) : null; + } + + public Optional> getAssignedPersons() { + return (this.assignPersons != null) + ? Optional.of(Collections.unmodifiableSet(this.assignPersons)) + : Optional.empty(); + } + + public Optional> getUnassignedPersons() { + return (this.unassignPersons != null) + ? Optional.of(Collections.unmodifiableSet(this.unassignPersons)) + : Optional.empty(); + } + + public void setGroups(Set groups) { + this.assignGroups = (groups != null) ? new HashSet<>(groups) : null; + } + + public Optional> getGroups() { + return (this.assignGroups != null) + ? Optional.of(Collections.unmodifiableSet(this.assignGroups)) + : Optional.empty(); + } + + public void setUnassignGroups(Set groups) { + this.unassignGroups = (groups != null) ? new HashSet<>(groups) : null; + } + + public Optional> getUnassignGroups() { + return (this.unassignGroups != null) + ? Optional.of(Collections.unmodifiableSet(this.unassignGroups)) + : Optional.empty(); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditPersonCommand.java b/src/main/java/seedu/address/logic/commands/EditPersonCommand.java new file mode 100644 index 00000000000..cd5cbba18e4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditPersonCommand.java @@ -0,0 +1,375 @@ +package seedu.address.logic.commands; + +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_GROUP; +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_REMARK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_UNASSIGN_GROUPS; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.group.Group; +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; +import seedu.address.model.person.Phone; +import seedu.address.model.person.Remark; + +/** + * Edits the details of an existing person in the address book. + */ +public class EditPersonCommand extends Command { + + public static final String COMMAND_WORD = "edit_person"; + + 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) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_PHONE + "PHONE] " + + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_BIRTHDAY + "BIRTHDAY] " + + "[" + PREFIX_REMARK + "REMARK] " + + "[" + PREFIX_GROUP + "GROUP]... " + + "[" + PREFIX_UNASSIGN_GROUPS + "GROUP]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_PHONE + "91234567 " + + PREFIX_EMAIL + "johndoe@example.com"; + + public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + + private final Index index; + private final EditPersonDescriptor editPersonDescriptor; + + private final Logger logger = LogsCenter.getLogger(EditPersonCommand.class); + + /** + * @param index of the person in the filtered person list to edit + * @param editPersonDescriptor details to edit the person with + */ + public EditPersonCommand(Index index, EditPersonDescriptor editPersonDescriptor) { + requireNonNull(index); + requireNonNull(editPersonDescriptor); + + this.index = index; + this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); + } + + @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 = createEditedPerson(personToEdit, editPersonDescriptor); + + // This check must happen first to check duplicate persons + if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + System.out.println("duplicate detected 2"); + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } + + model.setPerson(personToEdit, editedPerson); + + // Update assigned persons in the event list + model.updateAssignedPersons(personToEdit, editedPerson); + + /* Remove empty groups from event when un-assigning groups from persons + and that person is the last member of the group + */ + Set emptyGroups = model.getEmptyGroups(personToEdit); + if (!emptyGroups.isEmpty()) { + for (Group group : emptyGroups) { + logger.info(String.format("Removing empty group: %s", group)); + } + model.removeEmptyGroups(emptyGroups); + } else { + model.updateGroups(); + } + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} + * edited with {@code editPersonDescriptor}. + */ + private static Person createEditedPerson(Person personToEdit, + EditPersonDescriptor editPersonDescriptor) throws CommandException { + + assert personToEdit != null; + + Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); + Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); + Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); + Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + Birthday updatedBirthday = editPersonDescriptor.getBirthday().orElse(personToEdit.getBirthday()); + Remark updatedRemark = editPersonDescriptor.getRemark().orElse(personToEdit.getRemark()); + + Set updatedGroups = new HashSet<>(); + + LocalDate bd = updatedBirthday.getValue(); + if (bd != null && bd.isAfter(LocalDate.now())) { + throw new CommandException(String.format(Messages.MESSAGE_INVALID_BIRTHDAY, + updatedBirthday.getStringValue())); + } + + if (editPersonDescriptor.getGroups().isPresent()) { + updatedGroups.addAll(personToEdit.getGroups()); + updatedGroups.addAll(editPersonDescriptor.getGroups().get()); + } else { + updatedGroups.addAll(personToEdit.getGroups()); + } + + if (editPersonDescriptor.getUnassignGroups().isPresent()) { + if (personToEdit.getGroups().containsAll(editPersonDescriptor.getUnassignGroups().get())) { + updatedGroups.removeAll(editPersonDescriptor.getUnassignGroups().get()); + } else { + Set invalidGroups = + getInvalidGroups(personToEdit, editPersonDescriptor.getUnassignGroups().get()); + + throw new CommandException(String.format(Messages.MESSAGE_INVALID_UNASSIGN_GROUP, + listInvalidGroups(invalidGroups))); + } + updatedGroups.removeAll(editPersonDescriptor.getUnassignGroups().get()); + } + + + Optional phone = Optional.ofNullable(updatedPhone); + Optional email = Optional.ofNullable(updatedEmail); + Optional
address = Optional.ofNullable(updatedAddress); + Optional birthday = Optional.ofNullable(updatedBirthday); + Optional remark = Optional.ofNullable(updatedRemark); + return new Person(updatedName, phone, email, address, birthday, remark, updatedGroups); + } + + private static Set getInvalidGroups(Person personToEdit, Set groups) { + Set invalidGroups = new HashSet<>(); + for (Group group : groups) { + if (!personToEdit.getGroups().contains(group)) { + invalidGroups.add(group); + } + } + + return invalidGroups; + } + + private static String listInvalidGroups(Set invalidGroups) { + StringBuilder builder = new StringBuilder(); + for (Group group : invalidGroups) { + builder.append(group.toString()); + builder.append(", "); + } + + builder.delete(builder.length() - 2, builder.length()); //removes the last comma + return builder.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditPersonCommand)) { + return false; + } + + EditPersonCommand otherEditPersonCommand = (EditPersonCommand) other; + return index.equals(otherEditPersonCommand.index) + && editPersonDescriptor.equals(otherEditPersonCommand.editPersonDescriptor); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .add("editPersonDescriptor", editPersonDescriptor) + .toString(); + } + + /** + * Stores the details to edit the person with. Each non-empty field value will replace the + * corresponding field value of the person. + */ + public static class EditPersonDescriptor { + private Name name; + private Phone phone; + private Email email; + private Address address; + private Birthday birthday; + private Remark remark; + private Set groups; + + private Set unassignGroups; + + public EditPersonDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code groups} is used internally. + */ + public EditPersonDescriptor(EditPersonDescriptor toCopy) { + setName(toCopy.name); + setPhone(toCopy.phone); + setEmail(toCopy.email); + setAddress(toCopy.address); + setBirthday(toCopy.birthday); + setRemark(toCopy.remark); + setGroups(toCopy.groups); + setUnassignGroups(toCopy.unassignGroups); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, phone, email, address, birthday, remark, groups, unassignGroups); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setPhone(Phone phone) { + this.phone = phone; + } + + public Optional getPhone() { + return Optional.ofNullable(phone); + } + + public void setEmail(Email email) { + this.email = email; + } + + public Optional getEmail() { + return Optional.ofNullable(email); + } + + public void setAddress(Address address) { + this.address = address; + } + + public Optional
getAddress() { + return Optional.ofNullable(address); + } + public void setBirthday(Birthday birthday) { + this.birthday = birthday; + } + + public Optional getBirthday() { + return Optional.ofNullable(birthday); + } + + public void setRemark(Remark remark) { + this.remark = remark; + } + + public Optional getRemark() { + return Optional.ofNullable(remark); + } + + /** + * Sets {@code groups} to this object's {@code groups}. + * A defensive copy of {@code groups} is used internally. + */ + public void setGroups(Set groups) { + this.groups = (groups != null) ? new HashSet<>(groups) : null; + } + + /** + * Sets {@code groups} to this object's {@code unassignGroups}. + */ + public void setUnassignGroups(Set groups) { + this.unassignGroups = groups != null ? new HashSet<>(groups) : null; + } + + /** + * Returns an unmodifiable group set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code groups} is null. + */ + public Optional> getGroups() { + return (groups != null) ? Optional.of(Collections.unmodifiableSet(groups)) : Optional.empty(); + } + + /** + * Returns an unmodifiable group set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code groups} is null. + */ + public Optional> getUnassignGroups() { + return (unassignGroups != null) ? Optional.of(unassignGroups) + : Optional.empty(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditPersonDescriptor)) { + return false; + } + + EditPersonDescriptor otherEditPersonDescriptor = (EditPersonDescriptor) other; + return Objects.equals(name, otherEditPersonDescriptor.name) + && Objects.equals(phone, otherEditPersonDescriptor.phone) + && Objects.equals(email, otherEditPersonDescriptor.email) + && Objects.equals(address, otherEditPersonDescriptor.address) + && Objects.equals(birthday, otherEditPersonDescriptor.birthday) + && Objects.equals(remark, otherEditPersonDescriptor.remark) + && Objects.equals(groups, otherEditPersonDescriptor.groups); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", name) + .add("phone", phone) + .add("email", email) + .add("address", address) + .add("birthday", birthday) + .add("remark", remark) + .add("groups", groups) + .toString(); + } + + + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindAllCommand.java b/src/main/java/seedu/address/logic/commands/FindAllCommand.java new file mode 100644 index 00000000000..5ae406391cf --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindAllCommand.java @@ -0,0 +1,86 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.event.EventNameOrGroupContainsKeywordsPredicate; +import seedu.address.model.person.PersonNameOrGroupContainsKeywordsPredicate; + +/** + * Finds and lists all persons and events in address book whose name or group contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FindAllCommand extends Command { + + public static final String COMMAND_WORD = "find_all"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all items whose names or groups contain any " + + "of the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " alice bob charlie"; + + private final PersonNameOrGroupContainsKeywordsPredicate personPredicate; + private final EventNameOrGroupContainsKeywordsPredicate eventPredicate; + + /** + * Constructs the {@code FindAllCommand}. + * @param personPredicate predicate for person + * @param eventPredicate predicate for event + */ + public FindAllCommand(PersonNameOrGroupContainsKeywordsPredicate personPredicate, + EventNameOrGroupContainsKeywordsPredicate eventPredicate) { + this.personPredicate = personPredicate; + this.eventPredicate = eventPredicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + eventPredicate.setPersonList(model.getFullPersonList()); + model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredEventList(Model.PREDICATE_SHOW_ALL_EVENTS); + model.updateFilteredEventList(eventPredicate); + model.updateFilteredPersonList(personPredicate); + int personListSize = model.getFilteredPersonList().size(); + int eventListSize = model.getFilteredEventList().size(); + String stringMessage = getResultString(personListSize, eventListSize); + return new CommandResult( + String.format(stringMessage, personListSize, eventListSize)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FindAllCommand)) { + return false; + } + + FindAllCommand otherFindAllCommand = (FindAllCommand) other; + return personPredicate.equals(otherFindAllCommand.personPredicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", personPredicate) + .toString(); + } + + private String getResultString(int personListSize, int eventListSize) { + if (personListSize == 1 && eventListSize != 1) { + return Messages.MESSAGE_PERSON_AND_EVENTS_LISTED_OVERVIEW; + } else if (personListSize != 1 && eventListSize == 1) { + return Messages.MESSAGE_PERSONS_AND_EVENT_LISTED_OVERVIEW; + } else if (personListSize == 1 && eventListSize == 1) { + return Messages.MESSAGE_PERSON_AND_EVENT_LISTED_OVERVIEW; + } else { + return Messages.MESSAGE_PERSONS_AND_EVENTS_LISTED_OVERVIEW; + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindEventCommand.java b/src/main/java/seedu/address/logic/commands/FindEventCommand.java new file mode 100644 index 00000000000..f7e31c03820 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindEventCommand.java @@ -0,0 +1,62 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.event.EventNameOrGroupContainsKeywordsPredicate; + +/** + * Finds and lists all events in address book whose name contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FindEventCommand extends Command { + + public static final String COMMAND_WORD = "find_event"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all events whose names, groups or assigned " + + "persons contain any of the specified keywords (case-insensitive) and displays them " + + "as a list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " alice bob charlie"; + + private final EventNameOrGroupContainsKeywordsPredicate predicate; + + public FindEventCommand(EventNameOrGroupContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + predicate.setPersonList(model.getFullPersonList()); + model.updateFilteredEventList(predicate); + int listSize = model.getFilteredEventList().size(); + return new CommandResult( + String.format(listSize == 1 ? Messages.MESSAGE_EVENT_LISTED_OVERVIEW + : Messages.MESSAGE_EVENTS_LISTED_OVERVIEW, listSize)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FindEventCommand)) { + return false; + } + + FindEventCommand otherFindEventCommand = (FindEventCommand) other; + return predicate.equals(otherFindEventCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindPersonCommand.java similarity index 53% rename from src/main/java/seedu/address/logic/commands/FindCommand.java rename to src/main/java/seedu/address/logic/commands/FindPersonCommand.java index 72b9eddd3a7..8cbaef996dc 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindPersonCommand.java @@ -5,24 +5,24 @@ import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.PersonNameOrGroupContainsKeywordsPredicate; /** - * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Finds and lists all persons in address book whose name or group contains any of the argument keywords. * Keyword matching is case insensitive. */ -public class FindCommand extends Command { +public class FindPersonCommand extends Command { - public static final String COMMAND_WORD = "find"; + public static final String COMMAND_WORD = "find_person"; - 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" + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names or groups contain any " + + "of the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + "Example: " + COMMAND_WORD + " alice bob charlie"; - private final NameContainsKeywordsPredicate predicate; + private final PersonNameOrGroupContainsKeywordsPredicate predicate; - public FindCommand(NameContainsKeywordsPredicate predicate) { + public FindPersonCommand(PersonNameOrGroupContainsKeywordsPredicate predicate) { this.predicate = predicate; } @@ -30,8 +30,10 @@ public FindCommand(NameContainsKeywordsPredicate predicate) { public CommandResult execute(Model model) { requireNonNull(model); model.updateFilteredPersonList(predicate); + int listSize = model.getFilteredPersonList().size(); return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + String.format(listSize == 1 ? Messages.MESSAGE_PERSON_LISTED_OVERVIEW + : Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, listSize)); } @Override @@ -41,11 +43,11 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof FindCommand)) { + if (!(other instanceof FindPersonCommand)) { return false; } - FindCommand otherFindCommand = (FindCommand) other; + FindPersonCommand otherFindCommand = (FindPersonCommand) other; return predicate.equals(otherFindCommand.predicate); } diff --git a/src/main/java/seedu/address/logic/commands/ListAllCommand.java b/src/main/java/seedu/address/logic/commands/ListAllCommand.java new file mode 100644 index 00000000000..6af6a6446d0 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ListAllCommand.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EVENTS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import seedu.address.model.Model; + +/** + * Lists all persons and events in the address book to the user. + */ +public class ListAllCommand extends Command { + + public static final String COMMAND_WORD = "list_all"; + + public static final String MESSAGE_SUCCESS = "Listed all persons and events"; + + + /** + * Executes the command and returns the result message. + * @param model {@code Model} which the command should operate on. + * @return result message of the command execution. + */ + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredEventList(PREDICATE_SHOW_ALL_EVENTS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListEventsCommand.java b/src/main/java/seedu/address/logic/commands/ListEventsCommand.java new file mode 100644 index 00000000000..4523ff8d7ee --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ListEventsCommand.java @@ -0,0 +1,29 @@ +package seedu.address.logic.commands; + + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EVENTS; + +import seedu.address.model.Model; + +/** + * Lists all events in the address book to the user. + */ +public class ListEventsCommand extends Command { + + public static final String COMMAND_WORD = "list_events"; + + public static final String MESSAGE_SUCCESS = "Listed all events"; + + /** + * Executes the command and returns the result message. + * @param model {@code Model} which the command should operate on. + * @return result message of the command execution. + */ + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredEventList(PREDICATE_SHOW_ALL_EVENTS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListPersonsCommand.java similarity index 83% rename from src/main/java/seedu/address/logic/commands/ListCommand.java rename to src/main/java/seedu/address/logic/commands/ListPersonsCommand.java index 84be6ad2596..0bdc88d46c5 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListPersonsCommand.java @@ -8,9 +8,9 @@ /** * Lists all persons in the address book to the user. */ -public class ListCommand extends Command { +public class ListPersonsCommand extends Command { - public static final String COMMAND_WORD = "list"; + public static final String COMMAND_WORD = "list_persons"; public static final String MESSAGE_SUCCESS = "Listed all persons"; diff --git a/src/main/java/seedu/address/logic/commands/RemindCommand.java b/src/main/java/seedu/address/logic/commands/RemindCommand.java new file mode 100644 index 00000000000..cfc345312a2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RemindCommand.java @@ -0,0 +1,96 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EVENTS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.BirthdayWithinDaysPredicate; +import seedu.address.model.person.EventWithinDaysPredicate; + +/** + * Reminds the user of the upcoming birthdays and events in the next n number of days. + */ +public class RemindCommand extends Command { + + public static final String COMMAND_WORD = "remind"; + + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Reminds the user of the upcoming birthdays and events in the next n number of days. " + + "If no NUM_OF_DAYS is given, the default number of days is 7.\n" + + "Parameters: NUM_OF_DAYS (must be a positive integer and maximum of 999999999)\n" + + "Example: " + COMMAND_WORD + " 1"; + + + + public static final String MESSAGE_REMIND_SUCCESS = + "Showing all birthdays and events happening in the next %1$s days:"; + + private static final int DEFAULT_DAYS = 7; + + + private final BirthdayWithinDaysPredicate birthdayPredicate; + + private final EventWithinDaysPredicate eventPredicate; + + private final int days; + + + + /** + * Creates a RemindCommand to remind the user of the upcoming birthdays and events in the next n number of days. + */ + public RemindCommand(BirthdayWithinDaysPredicate birthdayPredicate, + EventWithinDaysPredicate eventPredicate, int days) { + this.birthdayPredicate = birthdayPredicate; + this.eventPredicate = eventPredicate; + if (days == 7) { + this.days = DEFAULT_DAYS; + } else { + this.days = days; + } + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + // Show everything first before filtering + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredEventList(PREDICATE_SHOW_ALL_EVENTS); + + // Events must be filtered before persons, if not persons will be filtered out of events + model.updateFilteredEventList(eventPredicate); + model.updateFilteredPersonList(birthdayPredicate); + return new CommandResult(String.format(MESSAGE_REMIND_SUCCESS, days)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof RemindCommand)) { + return false; + } + + RemindCommand otherRemindCommand = (RemindCommand) other; + return birthdayPredicate.equals(otherRemindCommand.birthdayPredicate) + && eventPredicate.equals(otherRemindCommand.eventPredicate) + && days == otherRemindCommand.days; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("birthdayPredicate", birthdayPredicate) + .add("eventPredicate", eventPredicate) + .add("days", days) + .toString(); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java deleted file mode 100644 index 4ff1a97ed77..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ /dev/null @@ -1,61 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Set; -import java.util.stream.Stream; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new AddCommand object - */ -public class AddCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the AddCommand - * and returns an AddCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public AddCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); - } - - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/AddEventCommandParser.java b/src/main/java/seedu/address/logic/parser/AddEventCommandParser.java new file mode 100644 index 00000000000..46719e7d80d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddEventCommandParser.java @@ -0,0 +1,61 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.AddPersonCommandParser.arePrefixesPresent; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; + +import java.util.Optional; +import java.util.Set; + +import seedu.address.logic.commands.AddEventCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.EventDate; +import seedu.address.model.event.EventName; +import seedu.address.model.event.EventTime; +import seedu.address.model.event.Meeting; +import seedu.address.model.group.Group; +import seedu.address.model.person.Name; + +/** + * Parses input arguments and creates a new AddMeetingCommand object + * @author Yuheng + */ +public class AddEventCommandParser implements Parser { + @Override + public AddEventCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_EVENT_NAME, PREFIX_DATE, + PREFIX_START_TIME, PREFIX_END_TIME, PREFIX_NAME, PREFIX_GROUP); + + if (!arePrefixesPresent(argMultimap, PREFIX_EVENT_NAME, PREFIX_DATE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddEventCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_NAME, PREFIX_DATE, PREFIX_START_TIME, PREFIX_END_TIME); + + EventName meetingName = ParserUtil.parseEventName(argMultimap.getValue(PREFIX_EVENT_NAME).get()); + EventDate eventDate = ParserUtil.parseEventDate(argMultimap.getValue(PREFIX_DATE).get()); + EventTime meetingStartTime = EventTime.NULL_EVENT_TIME; + EventTime meetingEndTime = EventTime.NULL_EVENT_TIME; + Set nameList = ParserUtil.parsePersonNames(argMultimap.getAllValues(PREFIX_NAME)); + Set groupList = ParserUtil.parseGroups(argMultimap.getAllValues(PREFIX_GROUP)); + + if (argMultimap.getValue(PREFIX_START_TIME).isPresent()) { + meetingStartTime = ParserUtil.parseEventTime(argMultimap.getValue(PREFIX_START_TIME).get()); + } + if (argMultimap.getValue(PREFIX_END_TIME).isPresent()) { + meetingEndTime = ParserUtil.parseEventTime(argMultimap.getValue(PREFIX_END_TIME).get()); + } + + Meeting meeting = new Meeting(meetingName, eventDate, + Optional.of(meetingStartTime), Optional.of(meetingEndTime), nameList, groupList); + + return new AddEventCommand(meeting); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddPersonCommandParser.java b/src/main/java/seedu/address/logic/parser/AddPersonCommandParser.java new file mode 100644 index 00000000000..1a2ef189263 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddPersonCommandParser.java @@ -0,0 +1,95 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.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_GROUP; +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_REMARK; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddPersonCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.group.Group; +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; +import seedu.address.model.person.Phone; +import seedu.address.model.person.Remark; + +/** + * Parses input arguments and creates a new AddCommand object + */ +public class AddPersonCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddPersonCommand parse(String args) throws ParseException { + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_BIRTHDAY, PREFIX_REMARK, PREFIX_GROUP); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddPersonCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_REMARK, PREFIX_BIRTHDAY); + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).orElse(null)); + Phone phone = Phone.NULL_PHONE; + Email email = Email.NULL_EMAIL; + Address address = Address.NULL_ADDRESS; + Birthday birthday = Birthday.NULL_BIRTHDAY; + Remark remark = Remark.NULL_REMARK; + + Set groupList = ParserUtil.parseGroups(argMultimap.getAllValues(PREFIX_GROUP)); + + if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { + phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); + } + if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { + email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); + } + if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { + address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + } + if (argMultimap.getValue(PREFIX_BIRTHDAY).isPresent()) { + birthday = ParserUtil.parseBirthday(argMultimap.getValue(PREFIX_BIRTHDAY).get()); + } + if (argMultimap.getValue(PREFIX_REMARK).isPresent()) { + remark = ParserUtil.parseRemark(argMultimap.getValue(PREFIX_REMARK).get()); + } + + Optional optionalPhone = Optional.of(phone); + Optional optionalEmail = Optional.of(email); + Optional
optionalAddress = Optional.of(address); + Optional optionalBirthday = Optional.of(birthday); + Optional optionalRemark = Optional.of(remark); + + Person person = new Person(name, optionalPhone, optionalEmail, optionalAddress, optionalBirthday, + optionalRemark, groupList); + + return new AddPersonCommand(person); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + public static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..60a96ff9e06 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -8,15 +8,23 @@ import java.util.regex.Pattern; import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddEventCommand; +import seedu.address.logic.commands.AddPersonCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.DeleteEventCommand; +import seedu.address.logic.commands.DeletePersonCommand; +import seedu.address.logic.commands.EditEventCommand; +import seedu.address.logic.commands.EditPersonCommand; import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindAllCommand; +import seedu.address.logic.commands.FindEventCommand; +import seedu.address.logic.commands.FindPersonCommand; import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ListAllCommand; +import seedu.address.logic.commands.ListEventsCommand; +import seedu.address.logic.commands.ListPersonsCommand; +import seedu.address.logic.commands.RemindCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -53,23 +61,35 @@ public Command parseCommand(String userInput) throws ParseException { switch (commandWord) { - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); + case AddPersonCommand.COMMAND_WORD: + return new AddPersonCommandParser().parse(arguments); - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); + case EditPersonCommand.COMMAND_WORD: + return new EditPersonCommandParser().parse(arguments); - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); + case DeletePersonCommand.COMMAND_WORD: + return new DeletePersonCommandParser().parse(arguments); case ClearCommand.COMMAND_WORD: return new ClearCommand(); - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); + case FindAllCommand.COMMAND_WORD: + return new FindAllCommandParser().parse(arguments); - case ListCommand.COMMAND_WORD: - return new ListCommand(); + case FindPersonCommand.COMMAND_WORD: + return new FindPersonCommandParser().parse(arguments); + + case FindEventCommand.COMMAND_WORD: + return new FindEventCommandParser().parse(arguments); + + case ListAllCommand.COMMAND_WORD: + return new ListAllCommand(); + + case ListEventsCommand.COMMAND_WORD: + return new ListEventsCommand(); + + case ListPersonsCommand.COMMAND_WORD: + return new ListPersonsCommand(); case ExitCommand.COMMAND_WORD: return new ExitCommand(); @@ -77,6 +97,18 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case AddEventCommand.COMMAND_WORD: + return new AddEventCommandParser().parse(arguments); + + case EditEventCommand.COMMAND_WORD: + return new EditEventCommandParser().parse(arguments); + + case DeleteEventCommand.COMMAND_WORD: + return new DeleteEventCommandParser().parse(arguments); + + case RemindCommand.COMMAND_WORD: + return new RemindCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..868f4d6f858 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -10,6 +10,14 @@ public class CliSyntax { public static final Prefix PREFIX_PHONE = new Prefix("p/"); public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); - public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_BIRTHDAY = new Prefix("b/"); + public static final Prefix PREFIX_REMARK = new Prefix("r/"); + public static final Prefix PREFIX_GROUP = new Prefix("g/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_START_TIME = new Prefix("s/"); + public static final Prefix PREFIX_END_TIME = new Prefix("e/"); + public static final Prefix PREFIX_EVENT_NAME = new Prefix("m/"); + public static final Prefix PREFIX_UNASSIGN_PERSONS = new Prefix("u/"); + public static final Prefix PREFIX_UNASSIGN_GROUPS = new Prefix("ug/"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteEventCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteEventCommandParser.java new file mode 100644 index 00000000000..ae19786db05 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteEventCommandParser.java @@ -0,0 +1,29 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteEventCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteMeetingCommand object + */ +public class DeleteEventCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteMeetingCommand + * @param args the arguments to be parsed + * @return a DeleteMeetingCommand object for execution + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteEventCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteEventCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteEventCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeletePersonCommandParser.java similarity index 71% rename from src/main/java/seedu/address/logic/parser/DeleteCommandParser.java rename to src/main/java/seedu/address/logic/parser/DeletePersonCommandParser.java index 3527fe76a3e..48a5d92a51a 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeletePersonCommandParser.java @@ -3,26 +3,26 @@ import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeletePersonCommand; import seedu.address.logic.parser.exceptions.ParseException; /** * Parses input arguments and creates a new DeleteCommand object */ -public class DeleteCommandParser implements Parser { +public class DeletePersonCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the DeleteCommand * and returns a DeleteCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ - public DeleteCommand parse(String args) throws ParseException { + public DeletePersonCommand parse(String args) throws ParseException { try { Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); + return new DeletePersonCommand(index); } catch (ParseException pe) { throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeletePersonCommand.MESSAGE_USAGE), pe); } } diff --git a/src/main/java/seedu/address/logic/parser/EditEventCommandParser.java b/src/main/java/seedu/address/logic/parser/EditEventCommandParser.java new file mode 100644 index 00000000000..ca3748557ae --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditEventCommandParser.java @@ -0,0 +1,84 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_END_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_UNASSIGN_GROUPS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_UNASSIGN_PERSONS; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditEventCommand; +import seedu.address.logic.commands.EditEventCommand.EditEventDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditMeetingCommand object + */ +public class EditEventCommandParser implements Parser { + + @Override + public EditEventCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_EVENT_NAME, PREFIX_DATE, + PREFIX_START_TIME, PREFIX_END_TIME, PREFIX_NAME, PREFIX_UNASSIGN_PERSONS, + PREFIX_GROUP, PREFIX_UNASSIGN_GROUPS); + + Index index; + + // Getting the index + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditEventCommand.MESSAGE_USAGE), pe); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_NAME, PREFIX_DATE, PREFIX_START_TIME, PREFIX_END_TIME); + + EditEventDescriptor editEventDescriptor = new EditEventDescriptor(); + + if (argMultimap.getValue(PREFIX_EVENT_NAME).isPresent()) { + editEventDescriptor.setName(ParserUtil.parseEventName(argMultimap.getValue(PREFIX_EVENT_NAME).get())); + } + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + editEventDescriptor.setDate(ParserUtil.parseEventDate(argMultimap.getValue(PREFIX_DATE).get())); + } + if (argMultimap.getValue(PREFIX_START_TIME).isPresent()) { + editEventDescriptor + .setStartTime(ParserUtil.parseEventTime(argMultimap.getValue(PREFIX_START_TIME).get())); + } + if (argMultimap.getValue(PREFIX_END_TIME).isPresent()) { + editEventDescriptor.setEndTime(ParserUtil.parseEventTime(argMultimap.getValue(PREFIX_END_TIME).get())); + } + + if (!argMultimap.getAllValues(PREFIX_NAME).isEmpty()) { + editEventDescriptor.setPersonNames(ParserUtil.parsePersonNames(argMultimap.getAllValues(PREFIX_NAME))); + } + + if (!argMultimap.getAllValues(PREFIX_UNASSIGN_PERSONS).isEmpty()) { + editEventDescriptor + .setUnassignPersons(ParserUtil.parsePersonNames(argMultimap.getAllValues(PREFIX_UNASSIGN_PERSONS))); + } + + if (!argMultimap.getAllValues(PREFIX_GROUP).isEmpty()) { + editEventDescriptor.setGroups(ParserUtil.parseGroups(argMultimap.getAllValues(PREFIX_GROUP))); + } + + if (!argMultimap.getAllValues(PREFIX_UNASSIGN_GROUPS).isEmpty()) { + editEventDescriptor + .setUnassignGroups(ParserUtil.parseGroups(argMultimap.getAllValues(PREFIX_UNASSIGN_GROUPS))); + } + + if (!editEventDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditEventCommand.MESSAGE_NOT_EDITED); + } + + return new EditEventCommand(index, editEventDescriptor); + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditPersonCommandParser.java similarity index 51% rename from src/main/java/seedu/address/logic/parser/EditCommandParser.java rename to src/main/java/seedu/address/logic/parser/EditPersonCommandParser.java index 46b3309a78b..9ba58e01c88 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditPersonCommandParser.java @@ -3,46 +3,51 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GROUP; 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 static seedu.address.logic.parser.CliSyntax.PREFIX_REMARK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_UNASSIGN_GROUPS; import java.util.Collection; -import java.util.Collections; import java.util.Optional; import java.util.Set; import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.EditPersonCommand; +import seedu.address.logic.commands.EditPersonCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; +import seedu.address.model.group.Group; /** * Parses input arguments and creates a new EditCommand object */ -public class EditCommandParser implements Parser { +public class EditPersonCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the EditCommand * and returns an EditCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ - public EditCommand parse(String args) throws ParseException { + public EditPersonCommand 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_REMARK, PREFIX_GROUP, PREFIX_UNASSIGN_GROUPS); Index index; try { index = ParserUtil.parseIndex(argMultimap.getPreamble()); } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditPersonCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_BIRTHDAY, PREFIX_ADDRESS, PREFIX_REMARK); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -58,28 +63,38 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + if (argMultimap.getValue(PREFIX_BIRTHDAY).isPresent()) { + editPersonDescriptor.setBirthday(ParserUtil.parseBirthday(argMultimap.getValue(PREFIX_BIRTHDAY).get())); + } + if (argMultimap.getValue(PREFIX_REMARK).isPresent()) { + editPersonDescriptor.setRemark(ParserUtil.parseRemark(argMultimap.getValue(PREFIX_REMARK).get())); + } + + parseGroupsForEdit(argMultimap.getAllValues(PREFIX_GROUP)).ifPresent(editPersonDescriptor::setGroups); + + parseGroupsForEdit(argMultimap.getAllValues(PREFIX_UNASSIGN_GROUPS)) + .ifPresent(editPersonDescriptor::setUnassignGroups); if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + throw new ParseException(EditPersonCommand.MESSAGE_NOT_EDITED); } - return new EditCommand(index, editPersonDescriptor); + return new EditPersonCommand(index, editPersonDescriptor); } /** - * Parses {@code Collection tags} into a {@code Set} 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} containing zero tags. + * Parses {@code Collection groups} into a {@code Set} if {@code groups} is non-empty. + * If {@code groups} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero groups. */ - private Optional> parseTagsForEdit(Collection tags) throws ParseException { - assert tags != null; + private Optional> parseGroupsForEdit(Collection groups) throws ParseException { + assert groups != null; - if (tags.isEmpty()) { + if (groups.isEmpty()) { return Optional.empty(); } - Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); + + return Optional.of(ParserUtil.parseGroups(groups)); } } diff --git a/src/main/java/seedu/address/logic/parser/FindAllCommandParser.java b/src/main/java/seedu/address/logic/parser/FindAllCommandParser.java new file mode 100644 index 00000000000..784e7ea903d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindAllCommandParser.java @@ -0,0 +1,37 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; +import java.util.List; + +import seedu.address.logic.commands.FindAllCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.EventNameOrGroupContainsKeywordsPredicate; +import seedu.address.model.person.PersonNameOrGroupContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FindAllCommand object + */ +public class FindAllCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindAllCommand + * and returns a FindAllCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindAllCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindAllCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + List keywordList = Arrays.asList(nameKeywords); + + return new FindAllCommand(new PersonNameOrGroupContainsKeywordsPredicate(keywordList), + new EventNameOrGroupContainsKeywordsPredicate(keywordList)); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/FindEventCommandParser.java b/src/main/java/seedu/address/logic/parser/FindEventCommandParser.java new file mode 100644 index 00000000000..ea4da5a4f74 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindEventCommandParser.java @@ -0,0 +1,33 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; + +import seedu.address.logic.commands.FindEventCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.EventNameOrGroupContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FindEventCommand object + */ +public class FindEventCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindEventCommand + * and returns a FindEventCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindEventCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindEventCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + + return new FindEventCommand(new EventNameOrGroupContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindPersonCommandParser.java similarity index 56% rename from src/main/java/seedu/address/logic/parser/FindCommandParser.java rename to src/main/java/seedu/address/logic/parser/FindPersonCommandParser.java index 2867bde857b..5ed3b446ed8 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindPersonCommandParser.java @@ -4,30 +4,30 @@ import java.util.Arrays; -import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindPersonCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.PersonNameOrGroupContainsKeywordsPredicate; /** - * Parses input arguments and creates a new FindCommand object + * Parses input arguments and creates a new FindPersonCommand object */ -public class FindCommandParser implements Parser { +public class FindPersonCommandParser implements Parser { /** - * Parses the given {@code String} of arguments in the context of the FindCommand + * Parses the given {@code String} of arguments in the context of the FindPersonCommand * and returns a FindCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ - public FindCommand parse(String args) throws ParseException { + public FindPersonCommand parse(String args) throws ParseException { String trimmedArgs = args.trim(); if (trimmedArgs.isEmpty()) { throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindPersonCommand.MESSAGE_USAGE)); } String[] nameKeywords = trimmedArgs.split("\\s+"); - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + return new FindPersonCommand(new PersonNameOrGroupContainsKeywordsPredicate(Arrays.asList(nameKeywords))); } } diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..6572583b049 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -9,11 +9,16 @@ import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.event.EventDate; +import seedu.address.model.event.EventName; +import seedu.address.model.event.EventTime; +import seedu.address.model.group.Group; 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; -import seedu.address.model.tag.Tag; +import seedu.address.model.person.Remark; /** * Contains utility methods used for parsing strings in the various *Parser classes. @@ -50,6 +55,7 @@ public static Name parseName(String name) throws ParseException { return new Name(trimmedName); } + /** * Parses a {@code String phone} into a {@code Phone}. * Leading and trailing whitespaces will be trimmed. @@ -62,7 +68,7 @@ public static Phone parsePhone(String phone) throws ParseException { if (!Phone.isValidPhone(trimmedPhone)) { throw new ParseException(Phone.MESSAGE_CONSTRAINTS); } - return new Phone(trimmedPhone); + return Phone.of(trimmedPhone); } /** @@ -77,7 +83,37 @@ public static Address parseAddress(String address) throws ParseException { if (!Address.isValidAddress(trimmedAddress)) { throw new ParseException(Address.MESSAGE_CONSTRAINTS); } - return new Address(trimmedAddress); + return Address.of(trimmedAddress); + } + + /** + * Parses a {@code String birthday} into an {@code Birthday}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if a 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 Birthday.of(trimmedBirthday); + } + + /** + * Parses a {@code String remark} into an {@code Remark}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if a given {@code remark} is invalid. + */ + public static Remark parseRemark(String remark) throws ParseException { + requireNonNull(remark); + String trimmedRemark = remark.trim(); + if (!Remark.isValidRemark(trimmedRemark)) { + throw new ParseException(Remark.MESSAGE_CONSTRAINTS); + } + return Remark.of(trimmedRemark); } /** @@ -92,33 +128,111 @@ public static Email parseEmail(String email) throws ParseException { if (!Email.isValidEmail(trimmedEmail)) { throw new ParseException(Email.MESSAGE_CONSTRAINTS); } - return new Email(trimmedEmail); + return Email.of(trimmedEmail); } /** - * Parses a {@code String tag} into a {@code Tag}. + * upParses a {@code String group} into a {@code Group}. * Leading and trailing whitespaces will be trimmed. * - * @throws ParseException if the given {@code tag} is invalid. + * @throws ParseException if the given {@code group} is invalid. + */ + public static Group parseGroup(String group) throws ParseException { + requireNonNull(group); + String trimmedGroup = group.trim(); + if (!Group.isValidGroupName(trimmedGroup)) { + throw new ParseException(Group.MESSAGE_CONSTRAINTS); + } + return new Group(trimmedGroup); + } + + /** + * Parses {@code Collection groups} into a {@code Set}. + */ + public static Set parseGroups(Collection groups) throws ParseException { + requireNonNull(groups); + final Set groupSet = new HashSet<>(); + for (String groupName : groups) { + if (groupName.isBlank()) { + throw new ParseException(Group.MESSAGE_CONSTRAINTS); + } + groupSet.add(parseGroup(groupName)); + } + return groupSet; + } + + /** + * Parses {@code Collection names} into a {@code Set}. + */ + public static Set parsePersonNames(Collection names) throws ParseException { + requireNonNull(names); + final Set nameSet = new HashSet<>(); + for (String name : names) { + if (name.isBlank()) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS); + } + nameSet.add(parseName(name)); + } + return nameSet; + } + + /** + * Parses a {@code String name} into an {@code EventName}. + */ + public static EventName parseEventName(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!EventName.isValidName(trimmedName)) { + throw new ParseException(EventName.MESSAGE_CONSTRAINTS); + } + return new EventName(trimmedName); + } + + /** + * Parses a {@code String date} into an {@code EventDate}. + */ + public static EventDate parseEventDate(String date) throws ParseException { + requireNonNull(date); + String trimmedDate = date.trim(); + if (!EventDate.isValidDate(trimmedDate)) { + throw new ParseException(EventDate.MESSAGE_CONSTRAINTS); + } + return new EventDate(trimmedDate); + } + + /** + * Parses a {@code String time} into an {@code EventTime}. */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { - throw new ParseException(Tag.MESSAGE_CONSTRAINTS); + public static EventTime parseEventTime(String time) throws ParseException { + requireNonNull(time); + String trimmedTime = time.trim(); + if (!EventTime.isValidTime(trimmedTime)) { + throw new ParseException(EventTime.MESSAGE_CONSTRAINTS); } - return new Tag(trimmedTag); + return EventTime.of(trimmedTime); } /** - * Parses {@code Collection tags} into a {@code Set}. + * Parses a {@code String days} into an {@code int}. */ - public static Set parseTags(Collection tags) throws ParseException { - requireNonNull(tags); - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); + public static int parseDays(String days) throws ParseException { + String trimmedDays = days.trim(); + + + if (trimmedDays.isEmpty() || days == null) { + return Integer.parseInt("7"); } - return tagSet; + + if (!StringUtil.isNonZeroUnsignedInteger(trimmedDays)) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + + int daysInt = Integer.parseInt(trimmedDays); + + if (daysInt > 999999999) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + + return daysInt; } } diff --git a/src/main/java/seedu/address/logic/parser/RemindCommandParser.java b/src/main/java/seedu/address/logic/parser/RemindCommandParser.java new file mode 100644 index 00000000000..dbe95c58eba --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RemindCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.RemindCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.BirthdayWithinDaysPredicate; +import seedu.address.model.person.EventWithinDaysPredicate; + +/** + * Parses input arguments and creates a new RemindCommand object + */ +public class RemindCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the RemindCommand + * and returns a RemindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public RemindCommand parse(String args) throws ParseException { + try { + int days = ParserUtil.parseDays(args); + return new RemindCommand(new BirthdayWithinDaysPredicate(days), + new EventWithinDaysPredicate(days), days); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemindCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..c62eeae706c 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -2,21 +2,29 @@ import static java.util.Objects.requireNonNull; +import java.util.HashSet; import java.util.List; +import java.util.Set; import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventList; +import seedu.address.model.group.Group; +import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; /** * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) + * Duplicates are not allowed for persons (by .isSamePerson comparison) */ public class AddressBook implements ReadOnlyAddressBook { private final UniquePersonList persons; + private final EventList events; + /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html @@ -26,6 +34,7 @@ public class AddressBook implements ReadOnlyAddressBook { */ { persons = new UniquePersonList(); + events = new EventList(); } public AddressBook() {} @@ -38,7 +47,7 @@ public AddressBook(ReadOnlyAddressBook toBeCopied) { resetData(toBeCopied); } - //// list overwrite operations + //=========== List overwrite operations =========================================================== /** * Replaces the contents of the person list with {@code persons}. @@ -48,6 +57,14 @@ public void setPersons(List persons) { this.persons.setPersons(persons); } + /** + * Replaces the contents of the event list with {@code events}. + * @param newEvents the list of events to be replaced with. + */ + public void setEvents(List newEvents) { + this.events.setEvents(newEvents); + } + /** * Resets the existing data of this {@code AddressBook} with {@code newData}. */ @@ -55,9 +72,10 @@ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); setPersons(newData.getPersonList()); + setEvents(newData.getEventList()); } - //// person-level operations + //=========== person-level operations =========================================================== /** * Returns true if a person with the same identity as {@code person} exists in the address book. @@ -67,6 +85,11 @@ public boolean hasPerson(Person person) { return persons.contains(person); } + public Set getEmptyGroups(Person person) { + requireNonNull(person); + return persons.isLastPersonGroup(person); + } + /** * Adds a person to the address book. * The person must not already exist in the address book. @@ -82,7 +105,6 @@ public void addPerson(Person p) { */ public void setPerson(Person target, Person editedPerson) { requireNonNull(editedPerson); - persons.setPerson(target, editedPerson); } @@ -94,7 +116,64 @@ public void removePerson(Person key) { persons.remove(key); } - //// util methods + //=========== event-level operations =========================================================== + + /** + * Replaces the given event {@code target} in the list with {@code editedEvent}. + * @param target event to be edited. {@code target} must exist in the address book. + * @param editedEvent event with the edited details. + */ + public void setEvent(Event target, Event editedEvent) { + requireNonNull(editedEvent); + Event targetEvent = removePersonsInGroups(editedEvent); + this.events.setEvent(target, targetEvent); + } + + /** + * Adds an event to the address book. + * @param event Event to be added. + */ + public void addEvent(Event event) { + // Get persons per group + Event targetEvent = removePersonsInGroups(event); + this.events.addEvent(targetEvent); + sort(); + } + + public void deleteEvent(Event event) { + this.events.remove(event); + } + + /** + * remove persons from the event if it is already in the group + * @param event event to operate on + * @return return the event after finishing + */ + public Event removePersonsInGroups(Event event) { + for (Group group: event.getGroups()) { + Set personNames = new HashSet<>(); + for (Person person: this.persons) { + if (person.getGroups().contains(group)) { + personNames.add(person.getName()); + } + } + event.getNames().removeAll(personNames); + } + return event; + } + + // ========== Group operations =========================================================== + + /** + * Retrieve a list of Persons in the group + * @param groupName Group to search by + * @return List of Persons in the group + */ + public ObservableList getPersonsByGroup(Group groupName) { + return this.persons.getPersonsByGroup(groupName); + } + + //=========== util methods =========================================================== @Override public String toString() { @@ -108,6 +187,11 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList getEventList() { + return events.asUnmodifiableObservableList(); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -127,4 +211,19 @@ public boolean equals(Object other) { public int hashCode() { return persons.hashCode(); } + + /** + * Sorts the list of events. + */ + public void sort() { + this.events.sort(); + } + + /** + * Returns a list of all the names that do not exist. + */ + public Set findInvalidNames(Set names) { + return this.persons.findInvalidNames(names); + } + } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..9674d9b7cb3 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,10 +1,14 @@ package seedu.address.model; import java.nio.file.Path; +import java.util.Set; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.event.Event; +import seedu.address.model.group.Group; +import seedu.address.model.person.Name; import seedu.address.model.person.Person; /** @@ -14,6 +18,9 @@ public interface Model { /** {@code Predicate} that always evaluate to true */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_EVENTS = unused -> true; + /** * Replaces user prefs data with the data in {@code userPrefs}. */ @@ -79,9 +86,75 @@ public interface Model { /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the full person list */ + ObservableList getFullPersonList(); + + /** Returns a view of the event list */ + ObservableList getFilteredEventList(); + + /** + * Replaces the given person {@code target} with {@code editedPerson} in the address book + * @param target event to be edited. {@code target} must exist in the address book. + * @param editedEvent event with the edited details. + */ + void setEvent(Event target, Event editedEvent); + + /** + * Deletes the given event. + * @param target event to be deleted. {@code target} must exist in the address book. + */ + void deleteEvent(Event target); + /** * Updates the filter of the filtered person list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Updates the filter of the event list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredEventList(Predicate predicate); + + /** + * Adds an event to the list of events. + * @param toAdd Event to be added. + */ + void addEvent(Event toAdd); + + /** + * Returns a set of names that are not found in the address book. + * @param names Set of names to be checked. + * @return Set of names that are not found in the address book. + */ + Set findInvalidNames(Set names); + + /** + * Returns a set of groups that are not found in the address book. + * @param groups Set of groups to be checked. + * @return Set of groups that are not found in the address book. + */ + Set findInvalidGroups(Set groups); + + Set getEmptyGroups(Person person); + + void removeEmptyGroups(Set emptyGroups); + + void updateGroups(); + + /** + * Updates any events where the person to edit is assigned to. + * @param personToEdit person to edit + * @param editedPerson person with the edited details + */ + void updateAssignedPersons(Person personToEdit, Person editedPerson); + + /** + * Overloaded method to update any events where the person to delete is assigned to. + * @param personToDelete person to delete + */ + void updateAssignedPersons(Person personToDelete); + + } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..b6172302c67 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,6 +4,9 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; import java.util.logging.Logger; @@ -11,6 +14,11 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.Messages; +import seedu.address.model.event.Event; +import seedu.address.model.event.Meeting; +import seedu.address.model.group.Group; +import seedu.address.model.person.Name; import seedu.address.model.person.Person; /** @@ -22,6 +30,7 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final FilteredList filteredEvents; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -33,7 +42,8 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + this.filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + this.filteredEvents = new FilteredList<>(this.addressBook.getEventList()); } public ModelManager() { @@ -111,6 +121,61 @@ public void setPerson(Person target, Person editedPerson) { addressBook.setPerson(target, editedPerson); } + @Override + public Set getEmptyGroups(Person target) { + requireAllNonNull(target); + return addressBook.getEmptyGroups(target); + } + + // ========== Event ====================================================================================== + + @Override + public void setEvent(Event target, Event editedEvent) { + requireAllNonNull(target, editedEvent); + + Predicate personPredicate = this.filteredPersons.getPredicate(); + Predicate eventPredicate = this.filteredEvents.getPredicate(); + + // Reset the current persons list first + this.filteredPersons.setPredicate(PREDICATE_SHOW_ALL_PERSONS); + this.filteredEvents.setPredicate(PREDICATE_SHOW_ALL_EVENTS); + this.addressBook.setEvent(target, editedEvent); + sort(); + this.filteredPersons.setPredicate(personPredicate); + this.filteredEvents.setPredicate(eventPredicate); + } + + @Override + public void deleteEvent(Event target) { + this.addressBook.deleteEvent(target); + } + + @Override + public void removeEmptyGroups(Set groups) { + for (Event event: filteredEvents) { + event.removeEmptyGroups(groups); + setEvent(event, event); + } + } + + @Override + public void updateGroups() { + // Get the predicate + Predicate personPredicate = this.filteredPersons.getPredicate(); + Predicate eventPredicate = this.filteredEvents.getPredicate(); + + // Reset the current persons list first + //Reset the current persons list first + this.filteredPersons.setPredicate(PREDICATE_SHOW_ALL_PERSONS); + this.filteredEvents.setPredicate(PREDICATE_SHOW_ALL_EVENTS); + for (Event event: filteredEvents) { + event.updateGroups(); + setEvent(event, event); + } + this.filteredPersons.setPredicate(personPredicate); + this.filteredEvents.setPredicate(eventPredicate); + } + //=========== Filtered Person List Accessors ============================================================= /** @@ -119,13 +184,113 @@ public void setPerson(Person target, Person editedPerson) { */ @Override public ObservableList getFilteredPersonList() { - return filteredPersons; + return this.filteredPersons; + } + + @Override + public ObservableList getFullPersonList() { + return this.addressBook.getPersonList(); + } + + /** + * Returns the list of events + * @return ArrayList of events + */ + @Override + public ObservableList getFilteredEventList() { + return this.filteredEvents; } @Override public void updateFilteredPersonList(Predicate predicate) { requireNonNull(predicate); - filteredPersons.setPredicate(predicate); + this.filteredPersons.setPredicate(predicate); + } + + @Override + public void updateFilteredEventList(Predicate predicate) { + requireNonNull(predicate); + this.filteredEvents.setPredicate(predicate); + } + + /** + * Adds an event to the address book. + * @param toAdd Event to be added. + */ + @Override + public void addEvent(Event toAdd) { + addressBook.addEvent(toAdd); + sort(); + updateFilteredEventList(PREDICATE_SHOW_ALL_EVENTS); + } + + private void sort() { + this.addressBook.sort(); + } + + @Override + public Set findInvalidNames(Set names) { + return this.addressBook.findInvalidNames(names); + } + + @Override + public Set findInvalidGroups(Set groups) { + Set invalidGroups = new HashSet<>(); + + for (Group group : groups) { + if (!checkGroupExists(group)) { + invalidGroups.add(group); + } + } + return invalidGroups; + } + + @Override + public void updateAssignedPersons(Person personToEdit, Person editedPerson) { + for (Event event : this.filteredEvents) { + if (event.getNames().contains(personToEdit.getName())) { + logger.info(String.format("Updating events that involves %s : ", personToEdit.getName()) + + Messages.formatEvent(event)); + this.setEvent(event, createUpdatedEvent(event, personToEdit, editedPerson)); + event.getNames().add(editedPerson.getName()); + } + } + } + + @Override + public void updateAssignedPersons(Person personToDelete) { + for (Event event : this.filteredEvents) { + if (event.getNames().contains(personToDelete.getName())) { + event.getNames().remove(personToDelete.getName()); + setEvent(event, event); //update event in the storage + } + } + } + + private Event createUpdatedEvent(Event event, Person personToEdit, Person editedPerson) { + //add other switch statements for future event types + event.getEventType().toString(); + return new Meeting(event.getName(), event.getStartDate(), + Optional.of(event.getStartTime()), Optional.of(event.getEndTime()), + event.getUpdatedNames(personToEdit.getName(), editedPerson.getName()), + event.getGroups()); + } + + private boolean checkGroupExists(Group group) { + Predicate personPredicate = this.filteredPersons.getPredicate(); + + // Reset the current persons list first + this.filteredPersons.setPredicate(PREDICATE_SHOW_ALL_PERSONS); + for (Person person : this.filteredPersons) { + if (person.getGroups().contains(group)) { + // Switch back to the previous filtered persons list + this.filteredPersons.setPredicate(personPredicate); + return true; + } + } + // Switch back to the previous filtered persons list + this.filteredPersons.setPredicate(personPredicate); + return false; } @Override @@ -140,9 +305,13 @@ public boolean equals(Object other) { } ModelManager otherModelManager = (ModelManager) other; - return addressBook.equals(otherModelManager.addressBook) - && userPrefs.equals(otherModelManager.userPrefs) - && filteredPersons.equals(otherModelManager.filteredPersons); + return this.addressBook.equals(otherModelManager.addressBook) + && this.userPrefs.equals(otherModelManager.userPrefs) + && this.filteredPersons.equals(otherModelManager.filteredPersons); } + @Override + public String toString() { + return this.filteredPersons.toString() + "\n" + this.filteredEvents.toString(); + } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..da362d1b28b 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,8 +1,10 @@ package seedu.address.model; import javafx.collections.ObservableList; +import seedu.address.model.event.Event; import seedu.address.model.person.Person; + /** * Unmodifiable view of an address book */ @@ -14,4 +16,6 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + ObservableList getEventList(); + } diff --git a/src/main/java/seedu/address/model/event/Event.java b/src/main/java/seedu/address/model/event/Event.java new file mode 100644 index 00000000000..4d3d54faab3 --- /dev/null +++ b/src/main/java/seedu/address/model/event/Event.java @@ -0,0 +1,181 @@ +package seedu.address.model.event; + + +import static seedu.address.model.event.EventTime.NULL_EVENT_TIME; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import seedu.address.model.group.Group; +import seedu.address.model.person.Name; + +/** + * Represents an Event in the address book. + */ +public abstract class Event { + + public static final String START_TIME_CONSTRAINTS = "You cannot enter a time that is before the current time!"; + public static final String END_TIME_CONSTRAINTS = "You cannot enter an end time that is before the start time!"; + + private Set names; + private EventDate startDate; + private Optional startTime; + private EventDate endDate; + private Optional endTime; + private EventName name; + private EventType eventType; + private Set groups; + + /** + * Constructor for events with optional start and end time + * @param name name of the event + * @param startDate start date of the event + * @param startTime start time of the event + * @param endDate end date of the event + * @param endTime end time of the event + * @param names names of the people attending the event + */ + public Event(EventType eventType, EventName name, EventDate startDate, Optional startTime, + EventDate endDate, Optional endTime, Set names, Set groups) { + this.eventType = eventType; + this.name = name; + this.startDate = startDate; + this.startTime = startTime; + this.endDate = endDate; + this.endTime = endTime; + this.names = names; + this.groups = groups; + } + + public EventType getEventType() { + return this.eventType; + } + + public Set getGroups() { + return this.groups; + } + + /** + * Gets the start date time of the event + * @return start date time of the event + */ + public EventDate getStartDate() { + return this.startDate; + } + + public EventTime getStartTime() { + return this.startTime.get(); + } + + /** + * Returns true if the event has a start time. + */ + public boolean hasStartTime() { + return !(this.startTime.get() == NULL_EVENT_TIME); + } + + + /** + * Returns true if the event has an end time. + * @return true if the event has an end time + */ + public boolean hasEndTime() { + return !(this.endTime.get() == NULL_EVENT_TIME); + } + + public EventDate getEndDate() { + return this.endDate; + } + + public EventTime getEndTime() { + return this.endTime.get(); + } + + /** + * Gets the name of the event + * @return name of the event + */ + public EventName getName() { + return this.name; + } + + /** + * Returns true if both events are of the same type and have the same name. + * @param event event to be compared + * @return true if both events are of the same type and have the same name + */ + public abstract boolean isSameEvent(Event event); + + public Set getNames() { + return this.names; + } + + /** + * Returns a set of updated person names when a person's name is edited. + * @param toEdit name of the person to be edited + * @param editedName edited name of the person + * @return set of updated person names + */ + public Set getUpdatedNames(Name toEdit, Name editedName) { + if (!this.names.contains(toEdit)) { + return this.names; + } + Set newNames = new HashSet<>(); + for (Name name : this.names) { + if (name.equals(toEdit)) { + newNames.add(editedName); + } else { + newNames.add(name); + } + } + return newNames; + } + + public void removeEmptyGroups(Set groups) { + this.groups.removeAll(groups); + } + + /** + * Forces the display to update + */ + public void updateGroups() { + this.groups = this.groups; + } + + /** + * Returns true if the event is overdue. + */ + public boolean isOverDue() { + if (!hasStartTime() && !hasEndTime()) { + if (LocalDateTime.now().isBefore(this.getStartDate().getDate().atTime(LocalTime.MAX))) { + return false; + } + return true; + } else if (!hasStartTime()) { + if (LocalDateTime.now().isBefore(this.getEndDate().getDate() + .atTime(this.getEndTime().getEventTime()))) { + return false; + } + return true; + } else if (!hasEndTime()) { + if (LocalDateTime.now().isBefore(this.getStartDate().getDate() + .atTime(this.getStartTime().getEventTime()))) { + return false; + } + return true; + } else { + if (LocalDateTime.now().isBefore(this.getStartDate().getDate() + .atTime(this.getStartTime().getEventTime()))) { + return false; + } + return true; + } + } + + public boolean hasStartDateWithinDays(int days) { + return this.startDate.isWithinDays(days); + } +} diff --git a/src/main/java/seedu/address/model/event/EventComparator.java b/src/main/java/seedu/address/model/event/EventComparator.java new file mode 100644 index 00000000000..ce4cdcfd670 --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventComparator.java @@ -0,0 +1,57 @@ +package seedu.address.model.event; + +import java.util.Comparator; + +/** + * Represents a EventComparator in the address book. + */ +public class EventComparator implements Comparator { + /** + * Compares its two arguments for order. + * @param event1 First event to be compared. + * @param event2 Second event to be compared + * @return a negative integer, zero, or a positive integer based on the LocalDatetime order. + */ + public int compare(Event event1, Event event2) { + if (event1.getStartDate().getDate().isBefore(event2.getStartDate().getDate())) { + return -1; + } else if (event1.getStartDate().getDate().isAfter(event2.getStartDate().getDate())) { + return 1; + } + + //compare the startTime if it is present + if (event1.hasStartTime() && event2.hasStartTime()) { + if (event1.getStartTime().getEventTime().isBefore(event2.getStartTime().getEventTime())) { + return -1; + } else if (event1.getStartTime().getEventTime().isAfter(event2.getStartTime().getEventTime())) { + return 1; + } + } + + //if one of the event has startTime, it will be placed before the other + if (event1.hasStartTime() && !event2.hasStartTime()) { + return -1; + } else if (event2.hasStartTime() && !event1.hasStartTime()) { + return 1; + } + + if (event1.hasEndTime() && event2.hasEndTime()) { + if (event1.getEndTime().getEventTime().isBefore(event2.getEndTime().getEventTime())) { + return -1; + } else if (event1.getEndTime().getEventTime().isAfter(event2.getEndTime().getEventTime())) { + return 1; + } + } + + //if one of the event has endTime, it will be placed before the other + + if (event1.hasEndTime() && !event2.hasEndTime()) { + return -1; + } else if (event2.hasEndTime() && !event1.hasEndTime()) { + return 1; + } + + return 0; + + } +} diff --git a/src/main/java/seedu/address/model/event/EventDate.java b/src/main/java/seedu/address/model/event/EventDate.java new file mode 100644 index 00000000000..b7616bd4c2b --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventDate.java @@ -0,0 +1,92 @@ +package seedu.address.model.event; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.ResolverStyle; + +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * DateTime class to represent the date and time of an event. + */ +public class EventDate { + public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd") + .withResolverStyle(ResolverStyle.STRICT); + + public static final String MESSAGE_CONSTRAINTS = + "Date should be in YYYY-mm-dd format, i.e. 2023-09-30, and it should not be blank"; + + private LocalDate date; + + /** + * Constructs an EventDate object. + * @param date + * @throws ParseException if the date is invalid. + */ + public EventDate(String date) throws ParseException { + try { + this.date = LocalDate.parse(date, DATE_FORMATTER); + } catch (Exception e) { + throw new ParseException(MESSAGE_CONSTRAINTS); + } + } + + /** + * Returns true if a given string is a valid date. + */ + public static boolean isValidDate(String trimmedDate) { + try { + LocalDate parsedDate = LocalDate.parse(trimmedDate, DATE_FORMATTER); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Gets the date of the event + * @return date of the event + */ + public LocalDate getDate() { + return this.date; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof EventDate)) { + return false; + } + + EventDate otherDate = (EventDate) other; + return this.date.isEqual(otherDate.date); + } + + @Override + public String toString() { + return this.date.format(DATE_FORMATTER); + } + + /** + * Returns a string representation of the date in a format for display. + * @return a string representation of the date in a format for display. + */ + public String forDisplay() { + return this.date.format(DateTimeFormatter.ofPattern("dd MMM yyyy")); + + } + + /** + * Returns true if the date is within the number of days specified. + * @param days number of days + * @return true if the date is within the number of days specified + */ + public boolean isWithinDays(int days) { + LocalDate endDate = LocalDate.now().plusDays(days + 1); + return (this.date.isBefore(endDate) && this.date.isAfter(LocalDate.now())) + || this.date.isEqual(LocalDate.now()); + } +} diff --git a/src/main/java/seedu/address/model/event/EventList.java b/src/main/java/seedu/address/model/event/EventList.java new file mode 100644 index 00000000000..9abb549a9cd --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventList.java @@ -0,0 +1,80 @@ +package seedu.address.model.event; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.event.exceptions.EventNotFoundException; + +/** + * Represents a list of events in the address book. Operations for managing the list of events are implemented here. + */ +public class EventList { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Adds an event to the list of events. + * @param event Event to be added. + */ + public void addEvent(Event event) { + requireNonNull(event); + this.internalList.add(event); + } + + /** + * Replaces the given event {@code target} in the list with {@code editedEvent}. + * @param target event to be edited. {@code target} must exist in the address book. + * @param editedEvent event with the edited details. + */ + public void setEvent(Event target, Event editedEvent) { + requireAllNonNull(target, editedEvent); + + int index = this.internalList.indexOf(target); + if (index == -1) { + throw new EventNotFoundException(); // Change to event exception + } + + this.internalList.set(index, editedEvent); + } + + /** + * Returns the event List + * @return ArrayList of events. + */ + public List getEventsList() { + return this.internalList; + } + + public void setEvents(List newEvents) { + this.internalList.setAll(newEvents); + } + + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + /** + * Removes the equivalent event from the list. + * @param target Event to be removed. + */ + public void remove(Event target) { + requireNonNull(target); + if (!this.internalList.remove(target)) { + throw new EventNotFoundException(); + } + } + + /** + * Sorts the list of events. + */ + public void sort() { + this.internalList.sort(new EventComparator()); + } + +} diff --git a/src/main/java/seedu/address/model/event/EventName.java b/src/main/java/seedu/address/model/event/EventName.java new file mode 100644 index 00000000000..668722d9176 --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventName.java @@ -0,0 +1,58 @@ +package seedu.address.model.event; + + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Event's name in the address book + */ +public class EventName { + public static final String MESSAGE_CONSTRAINTS = + "Event 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. + */ + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public final String name; + + /** + * Constructs a {@code Name}. + * + * @param name A valid name. + */ + public EventName(String name) { + requireNonNull(name); + checkArgument(isValidName(name), MESSAGE_CONSTRAINTS); + this.name = name; + } + + /** + * Returns true if a given string is a valid name. + */ + public static boolean isValidName(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return this.name; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EventName)) { + return false; + } + + EventName otherName = (EventName) other; + return this.name.equals(otherName.name); + } +} diff --git a/src/main/java/seedu/address/model/event/EventNameOrGroupContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/event/EventNameOrGroupContainsKeywordsPredicate.java new file mode 100644 index 00000000000..4bf80340791 --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventNameOrGroupContainsKeywordsPredicate.java @@ -0,0 +1,69 @@ +package seedu.address.model.event; + +import java.util.List; +import java.util.function.Predicate; + +import javafx.collections.ObservableList; +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; + +/** + * Tests that a {@code Event}'s {@code Name} matches any of the keywords given. + */ +public class EventNameOrGroupContainsKeywordsPredicate implements Predicate { + private final List keywords; + private ObservableList personList; + + /** + * Constructs the EventNameContainsKeywordsPredicate with provided keywords. + * + * @param keywords keywords from user input. + */ + public EventNameOrGroupContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + public void setPersonList(ObservableList personList) { + this.personList = personList; + } + + @Override + public boolean test(Event event) { + assert personList != null; + + return this.keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(event.getName().toString(), keyword) + || event.getGroups().stream().anyMatch(group -> + StringUtil.containsWordIgnoreCase(group.groupName, keyword)) + || event.getNames().stream().anyMatch(name -> StringUtil.containsWordIgnoreCase( + name.fullName, keyword)) + || personList.stream().anyMatch( + person -> event + .getGroups() + .stream() + .anyMatch(person.getGroups()::contains) + && StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EventNameOrGroupContainsKeywordsPredicate)) { + return false; + } + + EventNameOrGroupContainsKeywordsPredicate otherEventNameContainsKeywordsPredicate = + (EventNameOrGroupContainsKeywordsPredicate) other; + return keywords.equals(otherEventNameContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/event/EventTime.java b/src/main/java/seedu/address/model/event/EventTime.java new file mode 100644 index 00000000000..842fe867516 --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventTime.java @@ -0,0 +1,119 @@ +package seedu.address.model.event; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Represents the time of the event. + */ +public class EventTime { + + public static final String MESSAGE_CONSTRAINTS = + "The time must be in HHmm format, i.e. 2359"; + + public static final String TIME_REGEX = "([01][0-9]|2[0-3])[0-5][0-9]"; + + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HHmm"); + public static final EventTime NULL_EVENT_TIME; + + static { + try { + NULL_EVENT_TIME = new EventTime("0000"); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + private LocalTime eventTime; + + /** + * Constructs an EventTime object. + * @param eventTime the event time in HHmm format. + * @throws ParseException if the given eventTime does not follow the format. + */ + private EventTime(String eventTime) throws ParseException { + try { + this.eventTime = LocalTime.parse(eventTime, TIME_FORMATTER); + } catch (Exception e) { + throw new ParseException(MESSAGE_CONSTRAINTS); + } + } + + /** + * Returns true if a given string is a valid time. + * Because time is optional, a blank string is considered valid. + */ + public static boolean isValidTime(String trimmedTime) { + try { + //to represent the case of optional time. + if (trimmedTime.isBlank()) { + return true; + } else if (!trimmedTime.matches(TIME_REGEX)) { + return false; + } + + LocalTime parsedTime = LocalTime.parse(trimmedTime, TIME_FORMATTER); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Returns the event time. + * @return event time. + */ + public LocalTime getEventTime() { + return this.eventTime; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof EventTime)) { + return false; + } + + EventTime otherTime = (EventTime) other; + return this.eventTime.equals(otherTime.eventTime); + } + + /** + * Factory method for EventTime. + * @param eventTime the event time in HHmm format. + * @return an instance of EventTime. + * @throws ParseException if the given eventTime does not follow the format. + */ + public static EventTime of(String eventTime) throws ParseException { + if (!isValidTime(eventTime)) { + throw new ParseException(MESSAGE_CONSTRAINTS); + } + + if (eventTime.isBlank()) { + return NULL_EVENT_TIME; + } + + return new EventTime(eventTime); + } + + /** + * ToString for the event time. + * Kept as the same format as the input for json storage. + */ + @Override + public String toString() { + return eventTime.format(DateTimeFormatter.ofPattern("HHmm")); + } + + /** + * For display in the UI. + */ + public String forDisplay() { + return eventTime.format(DateTimeFormatter.ofPattern("HH:mm")); + } +} diff --git a/src/main/java/seedu/address/model/event/EventType.java b/src/main/java/seedu/address/model/event/EventType.java new file mode 100644 index 00000000000..5df45da6381 --- /dev/null +++ b/src/main/java/seedu/address/model/event/EventType.java @@ -0,0 +1,24 @@ +package seedu.address.model.event; + +import static java.util.Objects.requireNonNull; + +/** + * Represents the type of event in the address book. + */ +public class EventType { + private final String eventType; + + /** + * Constructor for the event type + * @param eventType type of the event + */ + public EventType(String eventType) { + requireNonNull(eventType); + this.eventType = eventType; + } + + @Override + public String toString() { + return this.eventType; + } +} diff --git a/src/main/java/seedu/address/model/event/Meeting.java b/src/main/java/seedu/address/model/event/Meeting.java new file mode 100644 index 00000000000..f2b279d7056 --- /dev/null +++ b/src/main/java/seedu/address/model/event/Meeting.java @@ -0,0 +1,63 @@ +package seedu.address.model.event; + +import java.util.Optional; +import java.util.Set; + +import seedu.address.model.group.Group; +import seedu.address.model.person.Name; + + +/** + * Represents a Meeting in the address book. + */ +public class Meeting extends Event { + + private static final String MEETING_EVENT_TYPE = "meeting"; + + /** + * Constructor for the meeting with optional start and end time + * @param name name of the meeting + * @param date date of the meeting + * @param startTime start time of the meeting + * @param endTime end time of the meeting + * @param names names of the people attending the meeting + */ + public Meeting(EventName name, EventDate date, Optional startTime, + Optional endTime, Set names, Set groups) { + super(new EventType(MEETING_EVENT_TYPE), name, date, startTime, date, endTime, names, groups); + } + + + /** + * Checks if the meeting is the same as another meeting + * @param other the other meeting to be compared to + * @return true if the meetings have the same name. + * + */ + @Override + public boolean isSameEvent(Event other) { + if (other == this) { + return true; + } + if (!(other instanceof Meeting)) { + return false; + } + return other.getName().equals(getName()); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Meeting)) { + return false; + } + + Meeting otherMeeting = (Meeting) other; + return otherMeeting.getName().equals(getName()) + && otherMeeting.getStartDate().equals(getStartDate()) + && otherMeeting.getStartTime().equals(getStartTime()) + && otherMeeting.getEndTime().equals(getEndTime()) + && otherMeeting.getNames().equals(getNames()) + && otherMeeting.getGroups().equals(getGroups()); + } + +} diff --git a/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java b/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java new file mode 100644 index 00000000000..7aef96b9be6 --- /dev/null +++ b/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java @@ -0,0 +1,8 @@ +package seedu.address.model.event.exceptions; + +/** + * Signals that the operation is unable to find the specified event. + */ +public class EventNotFoundException extends RuntimeException{ + +} diff --git a/src/main/java/seedu/address/model/group/Group.java b/src/main/java/seedu/address/model/group/Group.java new file mode 100644 index 00000000000..fada17f4306 --- /dev/null +++ b/src/main/java/seedu/address/model/group/Group.java @@ -0,0 +1,66 @@ +package seedu.address.model.group; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Group in the address book. + * Guarantees: immutable; name is valid as declared in {@link #isValidGroupName(String)} + */ +public class Group { + + public static final String MESSAGE_CONSTRAINTS = "Group names should be alphanumeric and should not be blank"; + public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + + public final String groupName; + + /** + * Constructs a {@code Group}. + * + * @param groupName A valid Group name. + */ + public Group(String groupName) { + requireNonNull(groupName); + checkArgument(isValidGroupName(groupName), MESSAGE_CONSTRAINTS); + this.groupName = groupName; + } + + /** + * Returns true if a given string is a valid group name. + */ + public static boolean isValidGroupName(String test) { + return test.matches(VALIDATION_REGEX); + } + + public String getGroupName() { + return groupName; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Group)) { + return false; + } + + Group otherGroup = (Group) other; + return groupName.equals(otherGroup.groupName); + } + + @Override + public int hashCode() { + return groupName.hashCode(); + } + + /** + * Format state as text for viewing. + */ + public String toString() { + return '[' + groupName + ']'; + } + +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 469a2cc9a1e..988c5d50b42 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -1,6 +1,5 @@ package seedu.address.model.person; -import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** @@ -9,15 +8,26 @@ */ public class Address { - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; + public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it can be blank"; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ + // Updated: Allow empty address, so whitespace is allowed. public static final String VALIDATION_REGEX = "[^\\s].*"; - public final String value; + public static final Address NULL_ADDRESS; + + static { + try { + NULL_ADDRESS = new Address(""); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } + } + + private final String value; /** * Constructs an {@code Address}. @@ -25,7 +35,6 @@ public class Address { * @param address A valid address. */ public Address(String address) { - requireNonNull(address); checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); value = address; } @@ -34,6 +43,9 @@ public Address(String address) { * Returns true if a given string is a valid email. */ public static boolean isValidAddress(String test) { + if (test.trim().equals("")) { + return true; + } return test.matches(VALIDATION_REGEX); } @@ -57,9 +69,24 @@ public boolean equals(Object other) { return value.equals(otherAddress.value); } + /** + * Factory method to create an Address object. + * @param address + * @return Address object + * @throws IllegalArgumentException + */ + public static Address of(String address) throws IllegalArgumentException { + if (!isValidAddress(address)) { + throw new IllegalArgumentException(MESSAGE_CONSTRAINTS); + } else if (address.isBlank()) { + return Address.NULL_ADDRESS; + } else { + return new Address(address); + } + } + @Override public int hashCode() { return value.hashCode(); } - } 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..cf9e8d83272 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Birthday.java @@ -0,0 +1,136 @@ +package seedu.address.model.person; + +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Represents a Person's birthday in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidBirthday(String)} + */ +public class Birthday { + + public static final String MESSAGE_CONSTRAINTS = + "Birthdays should only contain numbers, and it should be in yyyy-MM-dd format"; + public static final String VALIDATION_REGEX = "\\d{4}-\\d{2}-\\d{2}"; + public static final Birthday NULL_BIRTHDAY; + + static { + try { + NULL_BIRTHDAY = new Birthday(""); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } + } + private String stringValue; + private LocalDate value; + /** + * Constructs a {@code Birthday}. + * + * @param birthday A valid birthday. + */ + public Birthday(String birthday) { + checkArgument(isValidBirthday(birthday), MESSAGE_CONSTRAINTS); + this.stringValue = birthday; + if (birthday.trim().isEmpty()) { + this.value = null; + return; + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate parsedDate = LocalDate.parse(birthday, formatter); + this.value = parsedDate; + } + /** + * Returns true if a given string is a valid birthday. + */ + public static boolean isValidBirthday(String test) { + if (test.trim().isEmpty()) { + return true; + } + + if (test.matches(VALIDATION_REGEX)) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate parsedDate = LocalDate.parse(test, formatter); + return true; + } catch (java.time.format.DateTimeParseException e) { + return false; + } + } + return false; + } + + @Override + public String toString() { + return (value == null) ? "" : value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } + + public String getStringValue() { + return stringValue; + } + + public LocalDate getValue() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Birthday)) { + return false; + } + + Birthday otherBirthday = (Birthday) other; + return value.equals(otherBirthday.value); + } + + /** + * Factory method for Birthday. + * @param birthday the birthday in yyyy-MM-dd format. + * @return an instance of Birthday. + * @throws IllegalArgumentException if the given birthday does not follow the format. + */ + public static Birthday of(String birthday) throws IllegalArgumentException { + if (!isValidBirthday(birthday)) { + throw new IllegalArgumentException(MESSAGE_CONSTRAINTS); + } + + if (birthday.isBlank()) { + return NULL_BIRTHDAY; + } + + return new Birthday(birthday); + } + + /** + * Returns a string for display. + */ + public String forDisplay() { + return (value == null) ? "" : value.format(DateTimeFormatter.ofPattern("dd MMM yyyy")); + } + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Returns true if the birthday is within the next {@code days} days. + */ + public boolean isWithinDays(int days) { + if (value == null) { + return false; + } + LocalDate now = LocalDate.now(); + LocalDate endDate = LocalDate.now().plusDays(days + 1); + LocalDate birthday = value.withYear(now.getYear()); + if (birthday.isBefore(now)) { + birthday = birthday.plusYears(1); + } + return (birthday.isBefore(endDate) && birthday.isAfter(now)) || birthday.isEqual(now); + } +} diff --git a/src/main/java/seedu/address/model/person/BirthdayWithinDaysPredicate.java b/src/main/java/seedu/address/model/person/BirthdayWithinDaysPredicate.java new file mode 100644 index 00000000000..5692c88eecb --- /dev/null +++ b/src/main/java/seedu/address/model/person/BirthdayWithinDaysPredicate.java @@ -0,0 +1,42 @@ +package seedu.address.model.person; + +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Birthday} is within the specified number of days from today. + */ +public class BirthdayWithinDaysPredicate implements Predicate { + private final int days; + + public BirthdayWithinDaysPredicate(int days) { + this.days = days; + } + + @Override + public boolean test(Person person) { + return person.hasBirthdayWithinDays(days); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof BirthdayWithinDaysPredicate)) { + return false; + } + + BirthdayWithinDaysPredicate otherPredicate = (BirthdayWithinDaysPredicate) other; + return days == otherPredicate.days; + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("days", days).toString(); + } + +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index c62e512bc29..77286e06e42 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -1,15 +1,34 @@ package seedu.address.model.person; -import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Represents a Person's email in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} */ public class Email { - + public static final Email NULL_EMAIL; + static { + try { + NULL_EMAIL = new Email(""); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } + } private static final String SPECIAL_CHARACTERS = "+_.-"; + // alphanumeric and special characters + private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]+"; // alphanumeric characters except underscore + private static final String LOCAL_PART_REGEX = "^" + ALPHANUMERIC_NO_UNDERSCORE + "([" + SPECIAL_CHARACTERS + "]" + + ALPHANUMERIC_NO_UNDERSCORE + ")*"; + private static final String DOMAIN_PART_REGEX = ALPHANUMERIC_NO_UNDERSCORE + + "(-" + ALPHANUMERIC_NO_UNDERSCORE + ")*"; + private static final String DOMAIN_LAST_PART_REGEX = "(" + DOMAIN_PART_REGEX + "){2,}$"; // At least two chars + private static final String DOMAIN_REGEX = "(?=[^\\.]*\\.)" + "(" + DOMAIN_PART_REGEX + "\\.)*" + + DOMAIN_LAST_PART_REGEX; + public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " + "and adhere to the following constraints:\n" + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " @@ -21,17 +40,10 @@ public class Email { + " - end with a domain label at least 2 characters long\n" + " - have each domain label start and end with alphanumeric characters\n" + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any."; - // alphanumeric and special characters - private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]+"; // alphanumeric characters except underscore - private static final String LOCAL_PART_REGEX = "^" + ALPHANUMERIC_NO_UNDERSCORE + "([" + SPECIAL_CHARACTERS + "]" - + ALPHANUMERIC_NO_UNDERSCORE + ")*"; - private static final String DOMAIN_PART_REGEX = ALPHANUMERIC_NO_UNDERSCORE - + "(-" + ALPHANUMERIC_NO_UNDERSCORE + ")*"; - private static final String DOMAIN_LAST_PART_REGEX = "(" + DOMAIN_PART_REGEX + "){2,}$"; // At least two chars - private static final String DOMAIN_REGEX = "(" + DOMAIN_PART_REGEX + "\\.)*" + DOMAIN_LAST_PART_REGEX; - public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" + DOMAIN_REGEX; - public final String value; + private static final Pattern PATTERN = Pattern.compile(LOCAL_PART_REGEX + "@" + DOMAIN_REGEX); + + private final String value; /** * Constructs an {@code Email}. @@ -39,7 +51,6 @@ public class Email { * @param email A valid email address. */ public Email(String email) { - requireNonNull(email); checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); value = email; } @@ -48,7 +59,11 @@ public Email(String email) { * Returns if a given string is a valid email. */ public static boolean isValidEmail(String test) { - return test.matches(VALIDATION_REGEX); + if (test.trim().equals("")) { + return true; + } + Matcher newMatcher = PATTERN.matcher(test); + return newMatcher.matches(); } @Override @@ -71,9 +86,24 @@ public boolean equals(Object other) { return value.equals(otherEmail.value); } + /** + * Factory method to create an Email object. + * @param email + * @return Email object + * @throws IllegalArgumentException + */ + public static Email of(String email) throws IllegalArgumentException { + if (!isValidEmail(email)) { + throw new IllegalArgumentException(MESSAGE_CONSTRAINTS); + } else if (email.isBlank()) { + return Email.NULL_EMAIL; + } else { + return new Email(email); + } + } + @Override public int hashCode() { return value.hashCode(); } - } diff --git a/src/main/java/seedu/address/model/person/EventWithinDaysPredicate.java b/src/main/java/seedu/address/model/person/EventWithinDaysPredicate.java new file mode 100644 index 00000000000..812eeb22774 --- /dev/null +++ b/src/main/java/seedu/address/model/person/EventWithinDaysPredicate.java @@ -0,0 +1,42 @@ +package seedu.address.model.person; + +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.event.Event; + +/** + * Tests that a {@code Person}'s {@code Birthday} is within the specified number of days from today. + */ +public class EventWithinDaysPredicate implements Predicate { + private final int days; + + public EventWithinDaysPredicate(int days) { + this.days = days; + } + + @Override + public boolean test(Event event) { + return event.hasStartDateWithinDays(days); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EventWithinDaysPredicate)) { + return false; + } + + EventWithinDaysPredicate otherPredicate = (EventWithinDaysPredicate) other; + return days == otherPredicate.days; + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("days", days).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..9d4dab2aa7e 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -3,6 +3,9 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Represents a Person's name in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} @@ -10,13 +13,14 @@ public class Name { public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + "Names should only contain alphanumeric characters and spaces, and it should not be blank. \n" + + "Names should also not be purely numbers"; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final Pattern VALIDATION_PATTERN = Pattern.compile("(?=.*[a-zA-Z])[\\p{Alnum}][\\p{Alnum} ]*"); public final String fullName; @@ -35,7 +39,8 @@ public Name(String name) { * Returns true if a given string is a valid name. */ public static boolean isValidName(String test) { - return test.matches(VALIDATION_REGEX); + Matcher newMatcher = VALIDATION_PATTERN.matcher(test); + return newMatcher.matches(); } diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java deleted file mode 100644 index 62d19be2977..00000000000 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ /dev/null @@ -1,44 +0,0 @@ -package seedu.address.model.person; - -import java.util.List; -import java.util.function.Predicate; - -import seedu.address.commons.util.StringUtil; -import seedu.address.commons.util.ToStringBuilder; - -/** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. - */ -public class NameContainsKeywordsPredicate implements Predicate { - private final List keywords; - - public NameContainsKeywordsPredicate(List keywords) { - this.keywords = keywords; - } - - @Override - public boolean test(Person person) { - return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof NameContainsKeywordsPredicate)) { - return false; - } - - NameContainsKeywordsPredicate otherNameContainsKeywordsPredicate = (NameContainsKeywordsPredicate) other; - return keywords.equals(otherNameContainsKeywordsPredicate.keywords); - } - - @Override - public String toString() { - return new ToStringBuilder(this).add("keywords", keywords).toString(); - } -} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..069bf95401a 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -5,10 +5,11 @@ import java.util.Collections; import java.util.HashSet; import java.util.Objects; +import java.util.Optional; import java.util.Set; import seedu.address.commons.util.ToStringBuilder; -import seedu.address.model.tag.Tag; +import seedu.address.model.group.Group; /** * Represents a Person in the address book. @@ -18,23 +19,47 @@ public class Person { // Identity fields private final Name name; - private final Phone phone; - private final Email email; - + private final Optional phone; + private final Optional email; // Data fields - private final Address address; - private final Set tags = new HashSet<>(); + private final Optional
address; + private final Optional birthday; + private final Optional remark; + private final Set groups = new HashSet<>(); + + /** + * Every field need not be present and not null. + */ + public Person(Name name) { + requireAllNonNull(name); + this.name = name; + this.phone = Optional.empty(); + this.email = Optional.empty(); + this.address = Optional.empty(); + this.birthday = Optional.empty(); + this.remark = Optional.empty(); + } /** - * Every field must be present and not null. + * Constructor for Person with all fields present. + * @param name + * @param phone + * @param email + * @param address + * @param birthday + * @param remark + * @param groups */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Optional phone, Optional email, Optional
address, + Optional birthday, Optional remark, Set groups) { + requireAllNonNull(name, phone, email, address, birthday, groups); this.name = name; this.phone = phone; this.email = email; this.address = address; - this.tags.addAll(tags); + this.birthday = birthday; + this.remark = remark; + this.groups.addAll(groups); } public Name getName() { @@ -42,23 +67,31 @@ public Name getName() { } public Phone getPhone() { - return phone; + return phone.orElse(null); } public Email getEmail() { - return email; + return email.orElse(null); } public Address getAddress() { - return address; + return address.orElse(null); + } + + public Birthday getBirthday() { + return birthday.orElse(null); + } + + public Remark getRemark() { + return remark.orElse(null); } /** - * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * Returns an immutable group set, which throws {@code UnsupportedOperationException} * if modification is attempted. */ - public Set getTags() { - return Collections.unmodifiableSet(tags); + public Set getGroups() { + return Collections.unmodifiableSet(groups); } /** @@ -83,35 +116,88 @@ public boolean equals(Object other) { if (other == this) { return true; } - // instanceof handles nulls if (!(other instanceof Person)) { return false; } - Person otherPerson = (Person) other; return name.equals(otherPerson.name) && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) - && tags.equals(otherPerson.tags); + && birthday.equals(otherPerson.birthday) + && remark.equals(otherPerson.remark) + && groups.equals(otherPerson.groups); } @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, remark, groups); } @Override public String toString() { return new ToStringBuilder(this) .add("name", name) - .add("phone", phone) - .add("email", email) - .add("address", address) - .add("tags", tags) + .add("phone", this.getPhone()) + .add("email", this.getEmail()) + .add("address", this.getAddress()) + .add("birthday", this.getBirthday()) + .add("remark", this.getRemark()) + .add("groups", groups) .toString(); } + /** + * Returns true if the person has a phone number. + */ + public boolean hasPhone() { + return getPhone() != Phone.NULL_PHONE; + } + + /** + * Returns true if the person has an email. + */ + public boolean hasEmail() { + return getEmail() != Email.NULL_EMAIL; + } + + /** + * Returns true if the person has an address. + */ + public boolean hasAddress() { + return getAddress() != Address.NULL_ADDRESS; + } + /** + * Returns true if the person has a birthday. + */ + public boolean hasBirthday() { + return getBirthday() != Birthday.NULL_BIRTHDAY; + } + /** + * Returns true if the person has a remark. + */ + public boolean hasRemark() { + return getRemark() != Remark.NULL_REMARK; + } + /** + * Returns true if the person has groups. + */ + public boolean hasGroups() { + return !getGroups().isEmpty(); + } + + /** + * Returns true if the person has a birthday within the next {@code days} days. + */ + public boolean hasBirthdayWithinDays(int days) { + Birthday birthday = getBirthday(); + return birthday.isWithinDays(days); + } + + public boolean hasGroup(Group group) { + return this.getGroups().contains(group); + } + } diff --git a/src/main/java/seedu/address/model/person/PersonNameOrGroupContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/PersonNameOrGroupContainsKeywordsPredicate.java new file mode 100644 index 00000000000..f19b0ce5fdf --- /dev/null +++ b/src/main/java/seedu/address/model/person/PersonNameOrGroupContainsKeywordsPredicate.java @@ -0,0 +1,55 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Name} or {@code Group} matches any of the keywords given. + */ +public class PersonNameOrGroupContainsKeywordsPredicate implements Predicate { + private final List keywords; + + /** + * Constructs the NameContainsKeywordsPredicate with provided keywords. + * + * @param keywords keywords from user input. + */ + public PersonNameOrGroupContainsKeywordsPredicate(List keywords) { + assert keywords != null; + + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> (StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword) + || person.getGroups().stream().anyMatch( + group -> StringUtil.containsWordIgnoreCase(group.groupName, keyword)))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PersonNameOrGroupContainsKeywordsPredicate)) { + return false; + } + + PersonNameOrGroupContainsKeywordsPredicate otherNameOrGroupContainsKeywordsPredicate = + (PersonNameOrGroupContainsKeywordsPredicate) other; + return keywords.equals(otherNameOrGroupContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } + +} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d733f63d739..7f6759dad77 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -1,19 +1,29 @@ package seedu.address.model.person; -import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import seedu.address.logic.parser.exceptions.ParseException; + /** * Represents a Person's phone number in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} */ public class Phone { - - public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; + public static final String MESSAGE_CONSTRAINTS = "Phone numbers should only contain numbers without whitespaces, " + + "and it should be at least 3 digits long and no longer than 17 digits."; public static final String VALIDATION_REGEX = "\\d{3,}"; - public final String value; + public static final Phone NULL_PHONE; + + static { + try { + NULL_PHONE = new Phone(""); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } + } + + private final String value; /** * Constructs a {@code Phone}. @@ -21,7 +31,6 @@ public class Phone { * @param phone A valid phone number. */ public Phone(String phone) { - requireNonNull(phone); checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); value = phone; } @@ -30,6 +39,14 @@ public Phone(String phone) { * Returns true if a given string is a valid phone number. */ public static boolean isValidPhone(String test) { + //to represent the case of optional time. + if (test.trim().equals("")) { + return true; + } + + if (test.length() > 17) { + return false; + } return test.matches(VALIDATION_REGEX); } @@ -53,6 +70,20 @@ public boolean equals(Object other) { return value.equals(otherPhone.value); } + /** + * Factory method of Phone class. + */ + public static Phone of(String phone) throws ParseException { + if (!isValidPhone(phone)) { + throw new ParseException(MESSAGE_CONSTRAINTS); + } + if (phone.isBlank()) { + return Phone.NULL_PHONE; + } else { + return new Phone(phone); + } + } + @Override public int hashCode() { return value.hashCode(); diff --git a/src/main/java/seedu/address/model/person/Remark.java b/src/main/java/seedu/address/model/person/Remark.java new file mode 100644 index 00000000000..f22445ba3d1 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Remark.java @@ -0,0 +1,88 @@ +package seedu.address.model.person; + +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's address in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidRemark(String)} + */ +public class Remark { + + public static final String MESSAGE_CONSTRAINTS = "Remarks can take any values, and it can be blank"; + + public static final String VALIDATION_REGEX = "[^\\s].*"; + + public static final Remark NULL_REMARK; + + static { + try { + NULL_REMARK = new Remark(""); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } + } + + private final String value; + + /** + * Constructs an {@code Remark}. + * + * @param remark A valid remark. + */ + public Remark(String remark) { + checkArgument(isValidRemark(remark), MESSAGE_CONSTRAINTS); + value = remark; + } + + /** + * Returns true if a given string is a valid remark. + */ + public static boolean isValidRemark(String test) { + if (test.trim().equals("")) { + return true; + } + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Remark)) { + return false; + } + + Remark otherRemark = (Remark) other; + return value.equals(otherRemark.value); + } + + /** + * Factory method to create a Remark object. + * @param remark + * @return Remark object + * @throws IllegalArgumentException + */ + public static Remark of(String remark) throws IllegalArgumentException { + if (!isValidRemark(remark)) { + throw new IllegalArgumentException(MESSAGE_CONSTRAINTS); + } else if (remark.isBlank()) { + return Remark.NULL_REMARK; + } else { + return new Remark(remark); + } + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index cc0a68d79f9..5452a646403 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -3,11 +3,14 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Set; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import seedu.address.model.group.Group; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.model.person.exceptions.PersonNotFoundException; @@ -61,6 +64,7 @@ public void setPerson(Person target, Person editedPerson) { throw new PersonNotFoundException(); } + // Catch duplicate persons if (!target.isSamePerson(editedPerson) && contains(editedPerson)) { throw new DuplicatePersonException(); } @@ -79,6 +83,10 @@ public void remove(Person toRemove) { } } + /** + * Removes the equivalent person from the list. + * @param replacement replacement list + */ public void setPersons(UniquePersonList replacement) { requireNonNull(replacement); internalList.setAll(replacement.internalList); @@ -93,7 +101,6 @@ public void setPersons(List persons) { if (!personsAreUnique(persons)) { throw new DuplicatePersonException(); } - internalList.setAll(persons); } @@ -104,6 +111,40 @@ public ObservableList asUnmodifiableObservableList() { return internalUnmodifiableList; } + /** + * Get a list of persons based on group name + * @param groupName groupName to search by + * @return A copy of the persons list with the same group name + */ + public ObservableList getPersonsByGroup(Group groupName) { + ObservableList personsList = FXCollections.observableArrayList(); + FXCollections.copy(personsList, internalList); + personsList.filtered(person -> person.hasGroup(groupName)); + return personsList; + } + + /** + * Get a set of groups that are empty after deleting the person + * @param person person to check + * @return A set of groups that are empty + */ + public Set isLastPersonGroup(Person person) { + Set emptyGroups = new HashSet<>(); + for (Group group: person.getGroups()) { + boolean isEmptyGroup = true; + for (Person p: internalList) { + if (p.hasGroup(group) && !p.equals(person)) { + isEmptyGroup = false; + break; + } + } + if (isEmptyGroup) { + emptyGroups.add(group); + } + } + return emptyGroups; + } + @Override public Iterator iterator() { return internalList.iterator(); @@ -147,4 +188,29 @@ private boolean personsAreUnique(List persons) { } return true; } + + + /** + * Returns the set of invalid names that do not yet exist in the addressBook. + */ + public Set findInvalidNames(Set names) { + Set invalidNames = new HashSet<>(); + + for (Name name : names) { + boolean hasName = checkNameExists(name); + if (!hasName) { + invalidNames.add(name); + } + } + return invalidNames; + } + + private boolean checkNameExists(Name name) { + for (Person person : this.internalList) { + if (person.getName().equals(name)) { + return true; + } + } + return false; + } } diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java deleted file mode 100644 index f1a0d4e233b..00000000000 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ /dev/null @@ -1,62 +0,0 @@ -package seedu.address.model.tag; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Tag in the address book. - * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} - */ -public class Tag { - - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; - - public final String tagName; - - /** - * Constructs a {@code Tag}. - * - * @param tagName A valid tag name. - */ - public Tag(String tagName) { - requireNonNull(tagName); - checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); - this.tagName = tagName; - } - - /** - * Returns true if a given string is a valid tag name. - */ - public static boolean isValidTagName(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof Tag)) { - return false; - } - - Tag otherTag = (Tag) other; - return tagName.equals(otherTag.tagName); - } - - @Override - public int hashCode() { - return tagName.hashCode(); - } - - /** - * Format state as text for viewing. - */ - public String toString() { - return '[' + tagName + ']'; - } - -} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..da1cc2fd391 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,60 +1,157 @@ package seedu.address.model.util; import java.util.Arrays; +import java.util.HashSet; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDate; +import seedu.address.model.event.EventName; +import seedu.address.model.event.EventTime; +import seedu.address.model.event.Meeting; +import seedu.address.model.group.Group; 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; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import seedu.address.model.person.Remark; /** * Contains utility methods for populating {@code AddressBook} with sample data. */ 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")) + return new Person[]{new Person(new Name("Alex Yeoh"), + Optional.of(new Phone("87438807")), + Optional.of(new Email("alexyeoh@example.com")), + Optional.of(new Address("Blk 30 Geylang Street 29, #06-40")), + Optional.of(new Birthday("2001-12-20")), + Optional.of(new Remark("")), + getGroupSet("friends")), + new Person(new Name("Bernice Yu"), + Optional.of(new Phone("99272758")), + Optional.of(new Email("berniceyu@example.com")), + Optional.of(new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18")), + Optional.of(new Birthday("2001-12-21")), + Optional.of(new Remark("")), + getGroupSet("colleagues", "friends")), + new Person(new Name("Charlotte Oliveiro"), + Optional.of(new Phone("93210283")), + Optional.of(new Email("charlotte@example.com")), + Optional.of(new Address("Blk 11 Ang Mo Kio Street 74, #11-04")), + Optional.of(new Birthday("2001-12-22")), + Optional.of(new Remark("")), + getGroupSet("neighbours")), + new Person(new Name("David Li"), + Optional.of(new Phone("91031282")), + Optional.of(new Email("lidavid@example.com")), + Optional.of(new Address("Blk 436 Serangoon Gardens Street 26, #16-43")), + Optional.of(new Birthday("2001-12-23")), + Optional.of(new Remark("")), + getGroupSet("family")), + new Person(new Name("Irfan Ibrahim"), + Optional.of(new Phone("92492021")), + Optional.of(new Email("irfan@example.com")), + Optional.of(new Address("Blk 47 Tampines Street 20, #17-35")), + Optional.of(new Birthday("2001-12-24")), + Optional.of(new Remark("")), + getGroupSet("classmates")), + new Person(new Name("Roy Balakrishnan"), + Optional.of(new Phone("92624417")), + Optional.of(new Email("royb@example.com")), + Optional.of(new Address("Blk 45 Aljunied Street 85, #11-31")), + Optional.of(new Birthday("2001-12-25")), + Optional.of(new Remark("")), + getGroupSet("colleagues")), + new Person(new Name("Ken"), + Optional.of(new Phone("92624417")), + Optional.of(new Email("ken@example.com")), + Optional.of(new Address("Blk 124 Bukit Merah Rd, #08-31")), + Optional.of(new Birthday("2001-03-23")), + Optional.of(new Remark("")), + getGroupSet("Team2")), + new Person(new Name("Yuheng"), + Optional.of(new Phone("92624417")), + Optional.of(new Email("yuheng@example.com")), + Optional.of(new Address("Blk 17 Lor 7 Toa Payoh, #08-31")), + Optional.of(new Birthday("2001-08-23")), + Optional.of(new Remark("")), + getGroupSet("Team2")) }; } + /** + * Returns an event list containing the list of events given. + */ + public static Event[] getSampleEvents() { + try { + return new Event[]{new Meeting(new EventName("Group meeting"), + new EventDate("2023-10-10"), + Optional.of(EventTime.of("1000")), + Optional.of(EventTime.of("1200")), + getNameSet("Alex Yeoh", "Bernice Yu"), + getGroupSet("classmates") + ), new Meeting(new EventName("Lunch with friends"), + new EventDate("2023-12-10"), + Optional.of(EventTime.NULL_EVENT_TIME), + Optional.of(EventTime.NULL_EVENT_TIME), + new HashSet<>(), + getGroupSet("friends") + ), new Meeting(new EventName("Family dinner"), + new EventDate("2023-12-11"), + Optional.of(EventTime.of("1800")), + Optional.of(EventTime.NULL_EVENT_TIME), + getNameSet("David Li"), + new HashSet<>() + )}; + } catch (ParseException e) { + return new Event[]{}; + } + } + + + public static ReadOnlyAddressBook getSampleAddressBook() { AddressBook sampleAb = new AddressBook(); for (Person samplePerson : getSamplePersons()) { sampleAb.addPerson(samplePerson); } + + for (Event sampleEvent : getSampleEvents()) { + sampleAb.addEvent(sampleEvent); + } return sampleAb; } /** - * Returns a tag set containing the list of strings given. + * Returns a group set containing the list of strings given. */ - public static Set getTagSet(String... strings) { + public static Set getGroupSet(String... strings) { + if (strings.length == 0) { + return new HashSet<>(); + } else if (strings.length == 1 && strings[0].isEmpty()) { + return new HashSet<>(); + } return Arrays.stream(strings) - .map(Tag::new) + .map(Group::new) .collect(Collectors.toSet()); } + public static Set getNameSet(String... strings) { + if (strings.length == 0) { + return new HashSet<>(); + } else if (strings.length == 1 && strings[0].isEmpty()) { + return new HashSet<>(); + } + return Arrays.stream(strings) + .map(Name::new) + .collect(Collectors.toSet()); + } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedEvent.java b/src/main/java/seedu/address/storage/JsonAdaptedEvent.java new file mode 100644 index 00000000000..d65f57218bb --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedEvent.java @@ -0,0 +1,175 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +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.commons.util.ToStringBuilder; +import seedu.address.model.event.Event; +import seedu.address.model.event.EventDate; +import seedu.address.model.event.EventName; +import seedu.address.model.event.EventTime; +import seedu.address.model.event.Meeting; +import seedu.address.model.group.Group; +import seedu.address.model.person.Name; + + +/** + * Json-friendly version of {@link Event}. + */ +public class JsonAdaptedEvent { + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; + public static final String UNKNOWN_EVENT_TYPE = "Unknown event type!"; + + private final String eventType; + + private final String name; + private final String date; + private final String startTime; + private final String endTime; + + private final List assignedPersons = new ArrayList<>(); + + private final List assignedGroups = new ArrayList<>(); + + /** + * Constructs a {@code JsonAdaptedMeeting} with the given person details. + */ + @JsonCreator + public JsonAdaptedEvent(@JsonProperty("eventType") String eventType, + @JsonProperty("name") String name, + @JsonProperty("date") String date, + @JsonProperty("startTime") String startTime, + @JsonProperty("endTime") String endTime, + @JsonProperty("assignedPersons") List assignedPersons, + @JsonProperty("assignedGroups") List assignedGroups) { + this.eventType = eventType; + this.name = name; + this.date = date; + this.startTime = startTime; + this.endTime = endTime; + + if (assignedPersons != null) { + this.assignedPersons.addAll(assignedPersons); + } + + if (assignedGroups != null) { + this.assignedGroups.addAll(assignedGroups); + } + } + + /** + * Converts a given {@code Meeting} into this class for Json use. + */ + public JsonAdaptedEvent(Event source) { + this.eventType = source.getEventType().toString(); + this.name = source.getName().name; + this.date = source.getStartDate().toString(); + this.startTime = source.hasStartTime() ? source.getStartTime().toString() : ""; + this.endTime = source.hasEndTime() ? source.getEndTime().toString() : ""; + + this.assignedPersons.addAll(source.getNames().stream() + .map(JsonAdaptedName::new) + .collect(Collectors.toList())); + + this.assignedGroups.addAll(source.getGroups().stream() + .map(JsonAdaptedGroup::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted person. + */ + public Event toModelType() throws IllegalValueException { + + final List personNames = new ArrayList<>(); + + for (JsonAdaptedName name : assignedPersons) { + personNames.add(name.toModelType()); + } + + final List groups = new ArrayList<>(); + + for (JsonAdaptedGroup group : assignedGroups) { + groups.add(group.toModelType()); + } + + if (this.name == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, EventName.class.getSimpleName())); + } + if (!EventName.isValidName(name)) { + throw new IllegalValueException(EventName.MESSAGE_CONSTRAINTS); + } + final EventName modelName = new EventName(name); + + if (this.date == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, EventDate.class.getSimpleName())); + } + if (!EventDate.isValidDate(this.date)) { + throw new IllegalValueException(EventDate.MESSAGE_CONSTRAINTS); + } + final EventDate modelEventDate = new EventDate(this.date); + + if (this.startTime == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, EventTime.class.getSimpleName())); + } + if (!EventTime.isValidTime(this.startTime)) { + throw new IllegalValueException(EventTime.MESSAGE_CONSTRAINTS); + } + final EventTime modelEventStartTime = EventTime.of(this.startTime); + + if (this.endTime == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, EventTime.class.getSimpleName())); + } + if (!EventTime.isValidTime(this.endTime)) { + throw new IllegalValueException(EventTime.MESSAGE_CONSTRAINTS); + } + final EventTime modelEventEndTime = EventTime.of(this.endTime); + + final Set modelNames = new HashSet<>(personNames); + + final Set modelGroups = new HashSet<>(groups); + + // no other events for now + return checkEventType(modelName, modelEventDate, + Optional.of(modelEventStartTime), Optional.of(modelEventEndTime), modelNames, modelGroups); + } + + + private Event checkEventType(EventName eventName, + EventDate eventDate, + Optional startTime, + Optional endTime, + Set personNames, + Set groups) throws IllegalValueException { + + if (this.eventType.equals("meeting")) { + return new Meeting(eventName, eventDate, startTime, endTime, personNames, groups); + } + throw new IllegalValueException(UNKNOWN_EVENT_TYPE); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("event_type", this.eventType) + .add("name", this.name) + .add("date", this.date) + .add("start_time", this.startTime == null ? "" : this.startTime) + .add("end_time", this.endTime == null ? "" : this.endTime) + .toString(); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedGroup.java b/src/main/java/seedu/address/storage/JsonAdaptedGroup.java new file mode 100644 index 00000000000..8c5cd11433a --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedGroup.java @@ -0,0 +1,47 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.group.Group; + +/** + * Jackson-friendly version of {@link Group}. + */ +class JsonAdaptedGroup { + + private final String groupName; + + /** + * Constructs a {@code JsonAdaptedGroup} with the given {@code groupName}. + */ + @JsonCreator + public JsonAdaptedGroup(String groupName) { + this.groupName = groupName; + } + + /** + * Converts a given {@code Group} into this class for Jackson use. + */ + public JsonAdaptedGroup(Group source) { + groupName = source.groupName; + } + + @JsonValue + public String getGroupName() { + return groupName; + } + + /** + * Converts this Jackson-friendly adapted group object into the model's {@code Group} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted group. + */ + public Group toModelType() throws IllegalValueException { + if (!Group.isValidGroupName(groupName)) { + throw new IllegalValueException(Group.MESSAGE_CONSTRAINTS); + } + return new Group(groupName); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedName.java b/src/main/java/seedu/address/storage/JsonAdaptedName.java new file mode 100644 index 00000000000..2e302df4aba --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedName.java @@ -0,0 +1,46 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Name; + +/** + * Jackson-friendly version of {@link Name}. + */ +public class JsonAdaptedName { + private final String name; + + /** + * Constructs a {@code JsonAdaptedGroup} with the given {@code groupName}. + */ + @JsonCreator + public JsonAdaptedName(String name) { + this.name = name; + } + + /** + * Converts a given {@code Group} into this class for Jackson use. + */ + public JsonAdaptedName(Name source) { + name = source.fullName; + } + + @JsonValue + public String getName() { + return name; + } + + /** + * Converts this Jackson-friendly adapted group object into the model's {@code Group} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted group. + */ + public Name toModelType() throws IllegalValueException { + if (!Name.isValidName(name)) { + throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); + } + return new Name(name); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..8617695019d 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -10,12 +11,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.group.Group; 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; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import seedu.address.model.person.Remark; /** * Jackson-friendly version of {@link Person}. @@ -28,7 +31,9 @@ class JsonAdaptedPerson { private final String phone; private final String email; private final String address; - private final List tags = new ArrayList<>(); + private final String birthday; + private final String remark; + private final List groups = new ArrayList<>(); /** * Constructs a {@code JsonAdaptedPerson} with the given person details. @@ -36,13 +41,17 @@ class JsonAdaptedPerson { @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("birthday") String birthday, + @JsonProperty("remark") String remark, + @JsonProperty("groups") List groups) { this.name = name; this.phone = phone; this.email = email; this.address = address; - if (tags != null) { - this.tags.addAll(tags); + this.birthday = birthday; + this.remark = remark; + if (groups != null) { + this.groups.addAll(groups); } } @@ -51,11 +60,13 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone */ public JsonAdaptedPerson(Person source) { name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tags.addAll(source.getTags().stream() - .map(JsonAdaptedTag::new) + phone = source.hasPhone() ? source.getPhone().toString() : ""; + email = source.hasEmail() ? source.getEmail().toString() : ""; + address = source.hasAddress() ? source.getAddress().toString() : ""; + birthday = source.hasBirthday() ? source.getBirthday().toString() : ""; + remark = source.hasRemark() ? source.getRemark().toString() : ""; + groups.addAll(source.getGroups().stream() + .map(JsonAdaptedGroup::new) .collect(Collectors.toList())); } @@ -65,9 +76,9 @@ public JsonAdaptedPerson(Person source) { * @throws IllegalValueException if there were any data constraints violated in the adapted person. */ public Person toModelType() throws IllegalValueException { - final List personTags = new ArrayList<>(); - for (JsonAdaptedTag tag : tags) { - personTags.add(tag.toModelType()); + final List personGroups = new ArrayList<>(); + for (JsonAdaptedGroup group : groups) { + personGroups.add(group.toModelType()); } if (name == null) { @@ -102,8 +113,23 @@ public Person toModelType() throws IllegalValueException { } final Address modelAddress = new Address(address); - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } + 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); + if (remark == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Remark.class.getSimpleName())); + } + final Remark modelRemark = new Remark(remark); + + final Set modelGroups = new HashSet<>(personGroups); + return new Person(modelName, Optional.of(modelPhone), Optional.of(modelEmail), Optional.of(modelAddress), + Optional.of(modelBirthday), Optional.of(modelRemark), modelGroups); + } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/address/storage/JsonAdaptedTag.java deleted file mode 100644 index 0df22bdb754..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ /dev/null @@ -1,48 +0,0 @@ -package seedu.address.storage; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; - -/** - * Jackson-friendly version of {@link Tag}. - */ -class JsonAdaptedTag { - - private final String tagName; - - /** - * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. - */ - @JsonCreator - public JsonAdaptedTag(String tagName) { - this.tagName = tagName; - } - - /** - * Converts a given {@code Tag} into this class for Jackson use. - */ - public JsonAdaptedTag(Tag source) { - tagName = source.tagName; - } - - @JsonValue - public String getTagName() { - return tagName; - } - - /** - * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted tag. - */ - public Tag toModelType() throws IllegalValueException { - if (!Tag.isValidTagName(tagName)) { - throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(tagName); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java index 41e06f264e1..d548e1c2ad4 100644 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java @@ -76,5 +76,4 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro FileUtil.createIfMissing(filePath); JsonUtil.saveJsonFile(new JsonSerializableAddressBook(addressBook), filePath); } - } diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..b2370907640 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -11,6 +11,7 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.event.Event; import seedu.address.model.person.Person; /** @@ -22,13 +23,17 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; private final List persons = new ArrayList<>(); + private final List events = new ArrayList<>(); /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableAddressBook} with the given persons and meetings. + * Extracted from the database */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { + public JsonSerializableAddressBook(@JsonProperty("persons") List persons, + @JsonProperty("events") List events) { this.persons.addAll(persons); + this.events.addAll(events); } /** @@ -38,6 +43,7 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List { + + private static final String FXML = "EventListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Event event; + + @FXML + private HBox cardPane; + @FXML + private Label name; + @FXML + private Label id; + @FXML + private Label date; + @FXML + private Label timeDuration; + @FXML + private FlowPane names; + @FXML + private FlowPane groups; + + /** + * Creates an {@code EventCode} with the given {@code Event} and index to display. + */ + public EventCard(ObservableList personList, Event event, int displayedIndex) { + super(FXML); + this.event = event; + id.setText(displayedIndex + ". "); + name.setText(event.getName().name); + date.setText(event.getStartDate().forDisplay()); + + if (event.hasStartTime() && event.hasEndTime()) { + timeDuration.setText(String.format("%s - %s", + event.getStartTime().forDisplay(), + event.getEndTime().forDisplay())); + } else if (event.hasStartTime()) { + timeDuration.setText(String.format("Start: %s", + event.getStartTime().forDisplay())); + } else if (event.hasEndTime()) { + timeDuration.setText(String.format("End: %s", + event.getEndTime().forDisplay())); + } else { + timeDuration.setText(""); + } + + event.getNames().stream() + .sorted(Comparator.comparing(name -> name.fullName)) + .forEach(name -> names.getChildren().add(new Label(name.fullName))); + // groups + event.getGroups().stream().sorted(Comparator.comparing(Group::getGroupName)) + .forEach(group -> { + groups.getChildren().add(new Label(group.getGroupName())); + personList.forEach(person -> { + if (person.getGroups().contains(group)) { + Label newLabel = new Label(person.getName().toString()); + newLabel.setId("groupPersonName"); + groups.getChildren().add(newLabel); + } + }); + }); + } +} diff --git a/src/main/java/seedu/address/ui/EventListPanel.java b/src/main/java/seedu/address/ui/EventListPanel.java new file mode 100644 index 00000000000..62ac8818a27 --- /dev/null +++ b/src/main/java/seedu/address/ui/EventListPanel.java @@ -0,0 +1,57 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.event.Event; +import seedu.address.model.person.Person; + +/** + * Panel containing the list of events. + */ +public class EventListPanel extends UiPart { + private static final String FXML = "EventListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(EventListPanel.class); + + @FXML + private ListView eventListView; + + private ObservableList personList; + + /** + * Creates a {@code EventListPanel} with the given {@code ObservableList}. + */ + public EventListPanel(ObservableList eventList, ObservableList personList) { + super(FXML); + this.personList = personList; + eventListView.setItems(eventList); + eventListView.setCellFactory(listView -> new EventListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Event} using a {@code EventCard}. + */ + class EventListViewCell extends ListCell { + @Override + protected void updateItem(Event event, boolean empty) { + super.updateItem(event, empty); + + if (empty || event == null) { + setGraphic(null); + setText(null); + } else { + if (event.isOverDue()) { + setGraphic(new ExpiredEventCard(personList, event, getIndex() + 1).getRoot()); + } else { + setGraphic(new EventCard(personList, event, getIndex() + 1).getRoot()); + } + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/ExpiredEventCard.java b/src/main/java/seedu/address/ui/ExpiredEventCard.java new file mode 100644 index 00000000000..cf526aeac5c --- /dev/null +++ b/src/main/java/seedu/address/ui/ExpiredEventCard.java @@ -0,0 +1,82 @@ +package seedu.address.ui; + +import java.util.Comparator; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.event.Event; +import seedu.address.model.group.Group; +import seedu.address.model.person.Person; + +/** + * UI component that displays information of an expired event. + */ +public class ExpiredEventCard extends UiPart { + + private static final String FXML = "ExpiredEventCard.fxml"; + + public final Event event; + + @FXML + private HBox cardPane; + @FXML + private Label name; + @FXML + private Label id; + @FXML + private Label date; + @FXML + private Label timeDuration; + @FXML + private FlowPane names; + @FXML + private FlowPane groups; + /** + * Creates a {@code ExpiredEventCard} with the given {@code Event} and index to display. + * @param event + * @param displayedIndex + */ + public ExpiredEventCard(ObservableList personList, Event event, int displayedIndex) { + super(FXML); + this.event = event; + id.setText(displayedIndex + ". "); + name.setText(event.getName().name); + date.setText(event.getStartDate().forDisplay()); + + if (event.hasStartTime() && event.hasEndTime()) { + timeDuration.setText(String.format("%s - %s", + event.getStartTime().forDisplay(), + event.getEndTime().forDisplay())); + } else if (event.hasStartTime()) { + timeDuration.setText(String.format("Start: %s", + event.getStartTime().forDisplay())); + } else if (event.hasEndTime()) { + timeDuration.setText(String.format("End: %s", + event.getEndTime().forDisplay())); + } else { + timeDuration.setText(""); + } + + event.getNames().stream() + .sorted(Comparator.comparing(name -> name.fullName)) + .forEach(name -> names.getChildren().add(new Label(name.fullName))); + // groups + event.getGroups().stream().sorted(Comparator.comparing(Group::getGroupName)) + .forEach(group -> { + groups.getChildren().add(new Label(group.getGroupName())); + personList.forEach(person -> { + if (person.getGroups().contains(group)) { + Label newLabel = new Label(person.getName().toString()); + newLabel.setId("groupPersonName"); + groups.getChildren().add(newLabel); + } + }); + }); + } + + +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..c07377f1ddf 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2324s1-cs2103t-t12-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/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..355c420e299 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -32,6 +32,7 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private EventListPanel eventListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @@ -44,6 +45,9 @@ public class MainWindow extends UiPart { @FXML private StackPane personListPanelPlaceholder; + @FXML + private StackPane eventListPanelPlaceholder; + @FXML private StackPane resultDisplayPlaceholder; @@ -110,9 +114,13 @@ 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()); + eventListPanel = new EventListPanel(logic.getFilteredEventList(), logic.getFullPersonList()); + eventListPanelPlaceholder.getChildren().add(eventListPanel.getRoot()); + resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..0f8a43d2dcf 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -7,6 +7,7 @@ import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; import seedu.address.model.person.Person; /** @@ -15,6 +16,7 @@ public class PersonCard extends UiPart { private static final String FXML = "PersonListCard.fxml"; + private static final String TEXT_STYLE = "-fx-font-family:Segoe UI; -fx-font-size: 13px;"; /** * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. @@ -33,27 +35,54 @@ public class PersonCard extends UiPart { @FXML private Label id; @FXML - private Label phone; + private FlowPane groups; @FXML - private Label address; - @FXML - private Label email; - @FXML - private FlowPane tags; + private VBox mainBox; /** - * Creates a {@code PersonCode} with the given {@code Person} and index to display. + * Creates a {@code PersonCard} with the given {@code Person} and index to display. */ public PersonCard(Person person, int displayedIndex) { super(FXML); this.person = person; id.setText(displayedIndex + ". "); name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - person.getTags().stream() - .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + fillInfo(); + person.getGroups().stream() + .sorted(Comparator.comparing(group -> group.groupName)) + .forEach(group -> groups.getChildren().add(new Label(group.groupName))); + } + + /** + * Adds a {@code Person}'s info into {@code PersonCard}. + */ + public void fillInfo() { + Label info; + + if (!person.getPhone().toString().isEmpty()) { + info = new Label(person.getPhone().toString()); + info.setStyle(TEXT_STYLE); + mainBox.getChildren().add(info); + } + if (!person.getAddress().toString().isEmpty()) { + info = new Label(person.getAddress().toString()); + info.setStyle(TEXT_STYLE); + mainBox.getChildren().add(info); + } + if (!person.getEmail().toString().isEmpty()) { + info = new Label(person.getEmail().toString()); + info.setStyle(TEXT_STYLE); + mainBox.getChildren().add(info); + } + if (!person.getBirthday().toString().isEmpty()) { + info = new Label(person.getBirthday().toString()); + info.setStyle(TEXT_STYLE); + mainBox.getChildren().add(info); + } + if (!person.getRemark().toString().isEmpty()) { + info = new Label(person.getRemark().toString()); + info.setStyle(TEXT_STYLE); + mainBox.getChildren().add(info); + } } } diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index f4c501a897b..9398d091972 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -33,6 +33,7 @@ public PersonListPanel(ObservableList personList) { * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. */ class PersonListViewCell extends ListCell { + @Override protected void updateItem(Person person, boolean empty) { super.updateItem(person, empty); diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java index b577f829423..4af0b9b39bf 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/StatusBarFooter.java @@ -1,7 +1,6 @@ package seedu.address.ui; import java.nio.file.Path; -import java.nio.file.Paths; import javafx.fxml.FXML; import javafx.scene.control.Label; @@ -22,7 +21,7 @@ public class StatusBarFooter extends UiPart { */ public StatusBarFooter(Path saveLocation) { super(FXML); - saveLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); + saveLocationStatus.setText("Manage Your Life."); } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..20c73f295b9 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/FumbleLog.png"; private Logic logic; private MainWindow mainWindow; diff --git a/src/main/resources/images/FumbleLog.png b/src/main/resources/images/FumbleLog.png new file mode 100644 index 00000000000..31b08818fd0 Binary files /dev/null and b/src/main/resources/images/FumbleLog.png differ diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..0a7b034becf 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -99,6 +99,10 @@ -fx-padding: 0 0 0 0; } +.list-cell:filled:highlighted { + -fx-background-color: #008000; +} + .list-cell:filled:even { -fx-background-color: #3c3e3f; } @@ -107,8 +111,8 @@ -fx-background-color: #515658; } + .list-cell:filled:selected { - -fx-background-color: #424d5f; } .list-cell:filled:selected #cardPane { @@ -307,6 +311,14 @@ -fx-padding: 8 1 8 1; } +.info-flow-pane { +} + +.info-flow-pane .label { + -fx-font-size: 14; + -fx-font-family: "Segoe UI Light"; +} + #cardPane { -fx-background-color: transparent; -fx-border-width: 0; @@ -337,12 +349,12 @@ -fx-background-radius: 0; } -#tags { +#groups { -fx-hgap: 7; -fx-vgap: 3; } -#tags .label { +#groups .label { -fx-text-fill: white; -fx-background-color: #3e7b91; -fx-padding: 1 3 1 3; @@ -350,3 +362,28 @@ -fx-background-radius: 2; -fx-font-size: 11; } + +#names { + -fx-hgap: 7; + -fx-vgap: 3; +} + +#names .label{ + -fx-text-fill: white; + -fx-background-color: #00660f !important; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +#groupPersonName { + -fx-hgap: 7; + -fx-vgap: 3; + -fx-text-fill: white; + -fx-background-color: #00660f !important; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} diff --git a/src/main/resources/view/EventListCard.fxml b/src/main/resources/view/EventListCard.fxml new file mode 100644 index 00000000000..49dc756c397 --- /dev/null +++ b/src/main/resources/view/EventListCard.fxml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/EventListPanel.fxml b/src/main/resources/view/EventListPanel.fxml new file mode 100644 index 00000000000..ca5ee074cf9 --- /dev/null +++ b/src/main/resources/view/EventListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/ExpiredEventCard.fxml b/src/main/resources/view/ExpiredEventCard.fxml new file mode 100644 index 00000000000..4e43501b41b --- /dev/null +++ b/src/main/resources/view/ExpiredEventCard.fxml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..a95663e2175 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -1,20 +1,16 @@ - - - - - - - - - - - - + + + + + + + + + - + @@ -33,25 +29,33 @@ - + - + - + - + - - - - - - + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f5e812e25e6..ca80d51815e 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -8,13 +8,15 @@ + + - + @@ -25,12 +27,9 @@ - - -