diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 00000000000..ce9fe0f82ab
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,5 @@
+ignore:
+ - "src/main/java/seedu/address/ui"
+ - "src/main/java/seedu/address/model/util"
+ - "src/main/java/seedu/address/Main.java"
+ - "src/main/java/seedu/address/MainApp.java"
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 00000000000..1e1862b0e82
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,25 @@
+name: MarkBind Action
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ build_and_deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install Graphviz
+ run: sudo apt-get install graphviz
+ - name: Install Java
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ - name: Build & Deploy MarkBind site
+ uses: MarkBind/markbind-action@v2
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ rootDirectory: './docs'
+ baseUrl: '/tp' # replace with your repo name
+ version: '^5.1.0'
diff --git a/.gitignore b/.gitignore
index 284c4ca7cd9..eab4c7db6a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,4 @@ src/test/data/sandbox/
# MacOS custom attributes files created by Finder
.DS_Store
docs/_site/
+docs/_markbind/logs/
diff --git a/README.md b/README.md
index 13f5c77403f..edd2b07dfa2 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,13 @@
-[](https://github.com/se-edu/addressbook-level3/actions)
-
-
-
-* This is **a sample project for Software Engineering (SE) students**.
- Example usages:
- * as a starting point of a course project (as opposed to writing everything from scratch)
- * as a case study
-* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details.
- * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big.
- * It comes with a **reasonable level of user and developer documentation**.
-* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...).
-* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**.
-* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info.
+[](https://github.com/AY2324S1-CS2103-F13-4/tp/actions/workflows/gradle.yml)
+
+
+
+
+**UniMate** is a powerful desktop application designed to streamline contact and events management.
+Users can effortlessly manage their contacts, schedule events, and keep track of important information, all in one place.
+
+* For the detailed documentation of this project, see the **[UniMate Product Website](https://ay2324s1-cs2103-f13-4.github.io/tp/)**.
+* If you are interested about developing UniMate, the **[Developer Guide](https://ay2324s1-cs2103-f13-4.github.io/tp/DeveloperGuide.html)** is a good place to start.
+
+### Acknowledgements
+- This project is forked from the AddressBook-Level3 project created by [SE-EDU initiative](https://se-education.org).
diff --git a/build.gradle b/build.gradle
index a2951cc709e..89f3fc53dd3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -66,7 +66,11 @@ dependencies {
}
shadowJar {
- archiveFileName = 'addressbook.jar'
+ archiveFileName = '[CS2103-F13-4][UniMate].jar'
+}
+
+run {
+ enableAssertions = true
}
defaultTasks 'clean', 'test'
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000000..1748e487fbd
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,23 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+_markbind/logs/
+
+# Dependency directories
+node_modules/
+
+# Production build files (change if you output the build to a different directory)
+_site/
+
+# Env
+.env
+.env.local
+
+# IDE configs
+.vscode/
+.idea/*
+*.iml
diff --git a/docs/AboutUs.md b/docs/AboutUs.md
index 1c9514e966a..0cc7444bb28 100644
--- a/docs/AboutUs.md
+++ b/docs/AboutUs.md
@@ -1,59 +1,58 @@
---
-layout: page
-title: About Us
+ layout: default.md
+ title: "About Us"
---
+# 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`
+This project was done by a team of 4 members.
## Project team
-### John Doe
-
-
-
-[[homepage](http://www.comp.nus.edu.sg/~damithch)]
-[[github](https://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
-
-* Role: Project Advisor
-
-### Jane Doe
+### Li Hongguang
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](https://github.com/lihongguang00)]
+[[portfolio](team/lihongguang00.md)]
-* Role: Team Lead
-* Responsibilities: UI
+- Role: Developer
+- Responsibilities:
+ - Calendar System Developer
-### Johnny Doe
+### Andre Sim
-
+
-[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)]
+[[github](http://github.com/fallman2)]
+[[portfolio](team/fallman2.md)]
-* Role: Developer
-* Responsibilities: Data
+- Role: Developer
+- Responsibilities:
+ - Task List Developer
-### Jean Doe
+### Low Jun Hong
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](http://github.com/junhonglow)]
+[[portfolio](team/junhonglow.md)]
-* Role: Developer
-* Responsibilities: Dev Ops + Threading
+- Role: Developer
+- Responsibilities:
+ - Event List Developer
+ - Storage Developer
-### James Doe
+### Nicholas Lee
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[github](http://github.com/nicrandomlee)]
+[[portfolio](team/nicrandomlee.md)]
-* Role: Developer
-* Responsibilities: UI
+- Role: Developer
+- Responsibilities:
+ - Product Feature Enhancements
+ - Product Tester
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 13cf0faea16..32f6255f3b9 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -1,6 +1,8 @@
---
-layout: page
-title: Configuration guide
+ layout: default.md
+ title: "Configuration guide"
---
+# Configuration guide
+
Certain properties of the application can be controlled (e.g user preferences file location, logging level) through the configuration file (default: `config.json`).
diff --git a/docs/DevOps.md b/docs/DevOps.md
index d2fd91a6001..8228c845e86 100644
--- a/docs/DevOps.md
+++ b/docs/DevOps.md
@@ -1,12 +1,15 @@
---
-layout: page
-title: DevOps guide
+ layout: default.md
+ title: "DevOps guide"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+# DevOps guide
---------------------------------------------------------------------------------------------------------------------
+
+
+
+
## Build automation
diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md
index 8a861859bfd..3bdc5c25749 100644
--- a/docs/DeveloperGuide.md
+++ b/docs/DeveloperGuide.md
@@ -1,15 +1,25 @@
---
-layout: page
-title: Developer Guide
+layout: default.md
+title: "Developer Guide"
+pageNav: 3
---
-* Table of Contents
-{:toc}
+
+# UniMate 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}
+UniMate is based off the [AddressBook-Level3](https://github.com/se-edu/addressbook-level3) project created by the [SE-EDU initiative](https://se-education.org/).
+
+--------------------------------------------------------------------------------------------------------------------
+
+## **Note about terms used in DG**
+
+In this document, `UniMateCalendar` and `Calendar` are used synonymously.
--------------------------------------------------------------------------------------------------------------------
@@ -21,14 +31,9 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md).
## **Design**
-
-
-:bulb: **Tip:** The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams.
-
-
### Architecture
-
+
The ***Architecture Diagram*** given above explains the high-level design of the App.
@@ -53,7 +58,7 @@ The bulk of the app's work is done by the following four components:
The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`.
-
+
Each of the four main components (also shown in the diagram above),
@@ -62,7 +67,7 @@ Each of the four main components (also shown in the diagram above),
For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below.
-
+
The sections below give more details of each component.
@@ -70,9 +75,11 @@ The sections below give more details of each component.
The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java)
-
+
+
-The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI.
+
+The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter`, `BottomListPanel` 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)
@@ -89,34 +96,36 @@ The `UI` component,
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.
-
+
+
+
-
: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.
-
+**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. When `Logic` is called upon to execute a command, it is passed to an `UniMateParser` object which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command.
1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`.
1. The command can communicate with the `Model` when it is executed (e.g. to delete a person).
1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`.
Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command:
-
+
How the parsing works:
-* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object.
+* When called upon to parse a user command, the `UniMateParser` 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 `UniMateParser` returns back as a `Command` object.
* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing.
### Model component
**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java)
-
+
The `Model` component,
@@ -126,22 +135,24 @@ The `Model` component,
* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects.
* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components)
-
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+
-
+**Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
-
+
+
+
### Storage component
**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java)
-
+
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).
+* can save address book data, calendar data, task manager data and user preference data in JSON format, and read them back into corresponding objects.
+* inherits from `AddressBookStorage`,`CalendarStorage`, `TaskManagerStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed).
* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`)
### Common classes
@@ -154,6 +165,409 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa
This section describes some noteworthy details on how certain features are implemented.
+//@@author lihongguang00
+### Adding Events
+
+#### Implementation
+
+The addEvent feature is facilitated by the `Calendar` class. It allows the users to block out some time
+in their personal `Calendar` with some `Event` that has the following attributes:
+
+* `DESCRIPTION` — Brief description of the `Event`
+* `START_DATE_TIME` — Starting date and time of the `Event`
+* `END_DATE_TIME` — End date and time of the `Event`
+
+The syntax used to call this command is as follows: `addEvent d/DESCRIPTION ts/START_DATE_TIME te/END_DATE_TIME`,
+with the `START_DATE_TIME` and `END_DATE_TIME` in the `yyyy-MM-dd HH:mm` format. If any of the fields are missing
+or if the formatting is incorrect, an error message will be thrown along with usage instructions on the correct
+formatting.
+
+Given below is an example usage scenario and how the addEvent feature behaves at each step.
+
+Step 1. The user launches the application for the first time. The user's personal `Calendar` will be initialized
+as an empty calendar.
+
+Step 2. The user executes `addEvent d/Go to school ts/2023-10-26 08:00 te/2023-10-26 16:00` to add the `Event`
+of `Go to school` from `2023-10-26 8am` to `2023-10-26 4pm`. This will call the `UniMateParser#execute()`,
+passing in the user input from the user.
+
+Step 3. Since the command is an `addEvent` command, it passes the user input to `AddEventCommandParser#parse()`
+for parsing.
+
+Step 4. The `AddEventCommandParser#parse` command will parse the command into 3 argument fields —
+`DESCRIPTION`, `START_DATE_TIME` and `END_DATE_TIME`. The `DESCRIPTION` is passed into
+`ParserUtil#parseEventDescription()` to produce a `EventDescription` object, while the `START_DATE_TIME`
+and `END_DATE_TIME` are passed into `ParserUtil#parseEventPeriod()` to produce a `EventPeriod` object.
+
+**Note**: If the `DESCRIPTION` is empty, or the `START_DATE_TIME` or `END_DATE_TIME` are of invalid
+format, a `ParseException` will be thrown, displaying the appropriate command usage format.
+
+Step 5. The `EventDescription` and `EventPeriod` objects produced in Step 4 are then passed into
+the constructor for `Event`, creating an `Event` object with the respective user-defined attributes. This
+`Event` object is then passed into the constructor of the `AddEventCommand` object.
+
+Step 6. `AddEventCommand#execute()` is then called, and the calendar will check if there is an existing
+`Event` that has a conflicting timing with the new `Event` to be added. Since the calendar is empty,
+no errors will be raised and the user will see his new `Event` displayed in the UI in the `My Calendar`
+region.
+
+**Note**: Suppose there is a conflicting `Event` that already exists in the `Calendar`
+with the `Event` to be added, the new `Event` will not be added, and a message that states that there
+is a timing conflict will be reflected in the UI status bar.
+
+#### Design considerations:
+
+**Aspect: Appropriate time period of an event:**
+
+* **Alternative 1 (Current choice): No restriction on time period**
+ * Pros: Allows users to add multi-day `Event` such as multi-day business trips.
+ * Cons: For users that rarely have multi-day `Event`, having to type the date twice when calling the command might be troublesome.
+* **Alternative 2: Restrict events to a single day**
+ * Pros: User only has to type date once when calling command.
+ * Cons: For multi-day `Event`, user has to call the command multiple times for all the relevant days
+
+
+//@@author Fallman2
+### Deleting Events
+
+#### Implementation
+
+The deletion of events is facilitated by the `model::deleteEventAt` method and the `model::findEventAt` method. The
+former method deletes the event stored in the `Calendar` object which itself is an attribute of the `Model` object by
+calling a similar method `Calendar::deleteEventAt`. These methods take in a `LocalDateTime` object and finds the method
+within the `Calendar` object, then in the case of `model::deleteEventAt`, deletes the event. Given below is an example
+usage scenario of the command.
+
+Step 1. The user launches the application and creates an event.
+
+Step 2. The user executes `deleteEvent 2023-12-09 12:00` command to delete the event at that time. The `deleteEvent`
+command calls `Model#findEventAt(LocalDateTime)` to find an event at the specified date and time. This event is then stored as a
+variable `toDelete`.
+
+**Note**: If no event is found at the specified date and time at any point of the command execution, an
+`EventNotFoundException` is thrown which causes an error message to be displayed.
+
+Step 3. The command then calls `Model#deleteEventAt(LocalDateTime)`. This method calls similar methods of
+`Calendar#deleteEventAt(LocalDateTime)` which calls `AllDaysEventListManager#deleteEventAt(LocalDateTime)`.
+
+Step 4. The `AllDaysEventListManager` checks for an event at the specified date and time again, then checks for all days
+for which the event lasts for. Then, for each day, the event is removed from the `SingleDayEventList`.
+
+Step 5. The deleted event which was previously stored in as a variable is displayed in the `CommandResult` to show the
+user which command was deleted.
+
+**Design considerations**
+
+**Aspect: Appropriate input:**
+
+* **Alternative 1: Use Event start time as input:**
+ * Pros: Allows the user to more specifically select which event is deleted.
+ * Cons: The exact start time of the event has to be used to delete the event.
+* **Alternative 2 (Current Choice): Use any time within the event as input:**
+ * Pros: Allows for more flexibility in the time given for the event to be deleted.
+ * Cons: User may be confused on which event is being deleted.
+
+### Clearing all events within a time frame
+
+All events within a time frame can be cleared using the `ClearEvents` command.
+The command requires the start and end time of the time frame to be cleared as inputs.
+All events that fall within or intersect with the time frame (start time inclusive, end time exclusive) are selected to be deleted.
+This command also has an optional confirmation as input. If the confirmation is not present in the input, the deletion will not be executed and instead, the events
+that would have been executed are displayed in the result. The command can then be reentered with the confirmation to confirm the deletion.
+
+#### Implementation
+
+Given below is an example usage scenario of the command.
+
+Step 1. The user launches the application and creates multiple events.
+
+Step 2. The user executes `clearEvents ts/2023-02-03 12:00 te/2023-02-03 14:00` to view all events within the time range.
+
+Step 3. The `ClearEventsCommandParser` creates an `EventPeriod` representing the indicated time frame and creates a new `ClearEventsCommand` with this `EventPeriod`.
+
+**Note**: At this step, if no events are found within the `EventPeriod` given, a `CommandException` is thrown and an error message is displayed.
+
+Step 4. The `ClearEventsCommand` calls the `Model#eventsInRange(EventPeriod)` method to find all events in the specified time range.
+
+Step 5. All events within the time range are returned and displayed in the command result.
+
+Step 6. The user views all events that would be deleted.
+
+Step 7. The user executes `clearEvents ts/2023-02-03 12:00 te/2023-02-03 14:00 c/CONFIRMED` to confirm the deletion.
+
+Step 8. The `ClearEventsCommand` calls the `Model#eventsInRange(EventPeriod)` method to find all events in the specified time range.
+
+Step 9. For each event in range, the command calls the `Model#deleteEventsInRange(EventPeriod)` which deletes all events within the range.
+
+Step 10. All deleted events are displayed in the command result.
+
+
+
+### Calendar Comparison (compareCalendars)
+
+#### Implementation
+
+When the command `compareCalendars` is called, the arguments passed by the users are converted into a list of `Index` in `CompareCalendarByIndexCommandParser#parse()`.
+The list of `Index` is then passed as an argument into the constructor `CompareCalendarByIndexCommand#new()`. Subsequently, when `CompareCalendarByIndexCommand#execute()` is called,
+the list of `Person` stored in the AddressBook is retrieved using `ModelManger#getFilteredPersonList()`, and the `Person` associated with each `Index` is extracted
+with `List#get()`. Their respective `UniMateCalendar` is then extracted with `Person#getCalendar()`, and all the `UniMateCalendar` of the user and relevant contacts are
+merged and reduce into a single `UniMateCalendar` with `UniMateCalendar#combineCalendar()`. This combined `UniMateCalendar` is then used to produce the grey event cards
+seen in the pop-up comparison calendar window displayed to the user with `CalendarEventSpace#addSolidEventCards()`.
+
+Step 1. The user launches the application and creates an event in the current week.
+
+Step 2. The user creates an event in the current week for one of their contact. For this example, let's assume the contact is `Alex Yeoh` at `Index` 1 of the AddressBook.
+
+Step 3. The user executed `compareCalendars 1`. The `CompareCalendarByIndexCommandParser#parse()` creates a list of `Index` containing a single `Index` object from
+ created from the user's argument `1`, and passed it into `CompareCalendarByIndexCommand#new()`.
+
+Step 4. The `CompareCalendarByIndexCommand#execute()` will turn the `Index` list into a `Stream` with `Arrays#stream()`, and each `Index` in the stream will be
+converted into the `Person` with the given `Index` with `Stream#map()` and `List#get()` as the `map()` function input.
+
+Step 5. The stream is further converted into a `Stream` of `UniMateCalendar` with `Stream#map()` and `Person#getCalendar()` as the `map()` function input.
+
+Step 6. Eventually, we call `Stream#reduce()` with the user's `UniMateCalendar` as seed, merging all of the `UniMateCalendar` in the `Stream` into a single one with `UniMateCalendar#combineCalendar()`.
+
+Step 7. The resultant `UniMateCalendar` then has the `Event` stored in it converted into grey event cards with `CalendarEventSpace#addSolidEventCards()`, which
+reflects in the resultant pop-up comparison calendar window.
+
+
+
+**Design Consideration**
+
+**Aspect: Whether to show calendar on a new pop-up or in the main GUI:**
+
+* **Alternative 1 (Current choice): Pop-up window**
+ * Pros: Looks cleaner since the comparison calendar is in an isolated window.
+ * Cons: User needs to close the pop-up, which might be inconvenient.
+* **Alternative 2: Display the calendar on the main application GUI**
+ * Pros: Less application tabs for the user to manage.
+ * Cons: More clutter on the main GUI.
+
+
+### Contact Filtering
+
+#### Implementation
+
+The filtering function executed by `FilterCommand` is facilitated by the `PersonFilter` class.
+which itself is similar to the `EditPersonDescriptor` class found in `EditCommand.java`. It stores the fields by which
+the contacts are to be filtered and creates a predicate to facilitate the filtering. Notably, it implements the
+following operation:
+
+* `PersonFilter#matchesFilter(Person)` - Compares the values of the attributes of the `Person` to the strings stored as
+ attributes in the `PersonFilter` object. This method is later used as a lambda method to filter the contact list.
+
+Given below is an example of how the filter function works at each step.
+
+Step 1. The user executes `filter n/Bob t/CS` to filter contacts to see only people with "Bob" in their name and have at
+least 1 tag with "CS" in it. The input is passed to `UniMateParser` which then parses it with the `FilterCommandParser`.
+
+Step 2. The `FilterCommandParser` parses the input and creates a corresponding `PersonFilter` object with null for all
+parameters. It then sets all specified attributes of the created `PersonFilter` while leaving unspecified fields as
+null. The `FilterCommandParser` finally returns a newly created `FilterCommand` with the PersonFilter used in the
+constructor.
+
+Step 3. `FilterCommand#execute` is called. In this method, `model#updateFilteredPersonList` is called with
+`PersonFilter#matchesFilter` being used as the predicate. This updates the GUI and populates the filtered list with
+only `Person` objects that match the filter.
+
+Step 4. The number of people displayed is returned as a `CommandResult`.
+
+//@@author nicrandomlee
+### Contact Sorting
+
+#### Implementation
+The sort function executed by `SortCommand`.
+
+The sort function allows users to sort all persons in UniMate based on a given criteria, and has the following attributes:
+
+* `COMPARATOR` — AddressBook sorting criteria
+* `reverse` — Determines if sorting is by descending order
+
+The syntax used to call this command is as follows: `sort /COMPARATOR [/reverse]`,
+with the `COMPARATOR` being one of `/byname`, `/byemail`, `/byphone`, `/byaddress` to sort by name, email, phone and address respectively. If any of the fields are missing or if the formatting is incorrect, an error message will be thrown along with usage instructions on the correct formatting. The `/reverse` parameter is optional to sort in descending order instead.
+
+Given below is an example of how the sort function works at each step. We will simulate a user using the sort function to sort UniMate contacts by name in descending order.
+
+Step 1. The user executes `sort /byname /reverse` to find his friend's contact. The input is passed into `UniMateParser` which then parses it with the `SortCommandParser`.
+
+Step 2. The `SortCommandParser` parses the input and first checks for arguments provided. If the arguments are empty, invalid or in the wrong format, a helper message will appear to allow the user to reference the sample run case. The arguments are then matched by the keywords provided to determine the basis for sorting using a `SortComparator`. All the comparators are added into an ArrayList of `SortComparator` for `SortCommand` to parse.
+
+Step 3. `SortCommand` is initialized parses the array from step 2 to determine the basis of comparison when the command is executed. The `SortCommandParser` finally returns a newly created `SortCommand` consisting of a Person Comparator that decides the method of sorting for the UniMate address book.
+
+Step 4. `SortCommand#execute` is called. In this method, `model#sortPersonList` is called with the Person Comparator created in step 3. This in turn calls `AddressBook#sortPersons` which calls the storage function to save the contacts in the json file based on the sorted order.
+
+Step 5. The GUI then reads in the json file to obtain the order of addresses and populates the sorted list with the sorting criteria provided.
+
+Step 6. The success message is returned as a `CommandResult` and displayed on the GUI result display panel.
+
+Here's a sequence diagram to summarise the steps above:
+
+
+
+**Design considerations**
+
+* The design of the `sort` command is dependent on the structure of the `AddressBookStorage` object. Should the structure
+ of how the AddressBook objects are stored change, a new implementation will be required for the command.
+
+//@@author
+
+### TaskList Feature
+
+The task list feature is facilitated by 'TaskManager'. It extends a ReadOnlyTaskManager that will be used for
+saving users' tasks. The data of the TaskManager is contained in a `TaskList` object. Additionally, it implements the following operations:
+
+* `TaskManager#addTask(Task)` -- Adds a task to the current task list and saves it to memory.
+* `TaskManager#deleteTask(int)` -- Delete an existing task from the current task list as indicated by its index and saves the change to memory.
+* `TaskManager#sortTasksBy(String)` -- Sets the comparator by which the internal TaskList is sorted to one of two preset options.
+This method only accepts the strings `"Description"` or `"Deadline"` as input and throws an error otherwise.
+* `TaskManager#getTaskList()` -- Returns and exposes the internal `TaskList` as an unmodifiable `ObservableList` that can be 'observed'.
+
+These operations are exposed in the `Model` interface as `Model#addTask(Task)`, `Model#deleteTask(int)`, `Model#sortTasksBy` and `Model#getTaskList` respectively.
+
+A `Task` object consists of a `TaskDescription` and can have up to 1 `Deadline`.
+
+#### Adding Tasks
+
+The adding of tasks is facilitated by the `TaskManager#addTask(Task)` method.
+The method adds a `Task` to the `TaskList` object which itself is an attribute of the `TaskManager` object by
+calling a similar method `TaskList#addTask(Task)`.
+
+#### Implementation
+
+These methods take in a `Task` object and adds the `Task` object to the `TaskList`.
+Given below is an example usage scenario of the command.
+
+Step 1. The user launches the application.
+
+Step 2. The user executes `addTask d/CS2105 Assignment te/2023-12-12 12:00 ` command to add the task.
+
+**Note**: While `Tasks` can have the same `TaskDescription` or `Deadline`, they cannot have both the same `TaskDescription` and `Deadline`.
+
+Step 3. The `addTask` method in the model is called, which calls the `addTask` method in the TaskManager, adding the task to the `TaskList`.
+
+Step 4. The `TaskList` is saved to the memory.
+
+**Design considerations**
+
+* The design of the `addTask` command is such that a deadline is made optional since some tasks are recurring or do not have a specific deadline.
+
+### Viewing Tasks
+
+Tasks can only be viewed in the bottom panel of the GUI which, by default, shows the events list instead.
+The viewing of tasks is facilitated by the `SwitchListCommand` which interacts with the UI to swap the bottom list between the event list and the task list.
+
+#### Implementation
+
+Given below is an example usage scenario of the command.
+
+Step 1: The user launches the application.
+
+Step 2: The user executes `switchList` command to view the task list.
+
+**Note**: If the task list is already displayed, the command switches the list back to the event list instead.
+
+Step 3: The `MainWindow` class of the UI switches the bottom list to the task list. The task list will now display the task list, allowing the user to view all tasks.
+
+**Design considerations**
+
+**Aspect: Mode of display:**
+
+* **Alternative 1 (Current Choice): Display the task list in the same area as another UI component:**
+ * Pros: The UI will be less cluttered and can have smaller minimum space.
+ * Cons: Both the event list and the task list cannot be viewed at the same time.
+* **Alternative 2: Display the task list in a separate area of the UI:**
+ * Pros: Both the event list and the task list can be viewed at the same time.
+ * Cons: The UI may be more cluttered.
+
+#### Deleting Tasks
+
+The deletion of tasks is facilitated by the `TaskManager#deleteTask(int)` method.
+The method deletes a `Task` from the `TaskList` object which is an attribute of the `TaskManager` object by calling a similar method `TaskList#deleteTask(int)`.
+This in turn deletes the task in `TaskList` that is indicated by its index within the `TaskList`.
+
+#### Implementation
+
+Given below is an example usage scenario of the command.
+
+Step 1. The user launches the application.
+
+Step 2. The user executes `switchList` command to view the task list on the GUI.
+
+Step 3. The user executes `deleteTask 1` command to delete the first task shown in the task list as displayed in the GUI.
+
+Step 4. The `deleteTask` method in the model is called, which calls the `deleteTask` method in the TaskManager, deleting the task from the `TaskList`.
+
+Step 5. The `TaskList` is saved to the memory.
+
+**Design considerations**
+
+**Aspect: Appropriate Input:**
+
+* **Alternative 1: Use Task description and deadline as input:**
+ * Pros: User does not need to refer to the GUI to confirm which task is being deleted.
+ * Cons: The exact description and deadlines have to be provided since tasks can have the same description or deadline.
+* **Alternative 2 (Current Choice): Use displayed index as input:**
+ * Pros: Much less input is required from the user.
+ * Cons: The user has to view the task list displayed in the GUI to confirm which task is being deleted.
+
+### Sorting Tasks
+
+#### Implementation
+
+Tasks in the TaskList can be sorted either alphabetically by description or by deadlines. The accepted inputs for these are `sortTasks Description` and `sortTasks Deadline` respectively.
+The sorting of tasks is facilitated by the `sortTasks` command.
+Given below is an example usage scenario of the command.
+
+Step 1. The user launches the application.
+
+Step 2. The user switches to the task list with the `switchList` command.
+
+Step 3. The user executes the `sortTasks Description` command to sort tasks by description alphabetically.
+
+Step 4. A `SortTasksCommand` is created after the command is parsed by the `SortTasksCommandParser`.
+
+Step 5. Upon execution, the `SortTasksCommand` calls `Model#sortTasks(String)` and passes the sorting method of `Description` represented by a String to it.
+
+Step 6. The `TaskManager#sortTasks(String)` method is called and the `sortingOrder` attribute of type `Comparator` of the `TaskManager` is set to the appropriate type.
+
+Step 7. The internal `TaskList` is sorted by the `sortingOrder`.
+
+//@@author nicrandomlee
+### Edit Contact Event
+
+#### Implementation
+The Edit Contact Event function executed by `editContactEvent` allows users to edit all person's calendar events in UniMate, and has the following attributes:
+
+* `PERSON_INDEX` — Index of the target person
+* `EVENT_INDEX` — Index of the target person's calendar event
+* `DESCRIPTION` — Brief description of the edited `Event`
+* `NEW_START_DATE_TIME` — Starting date and time of the edited `Event`
+* `NEW_END_DATE_TIME` — End date and time of the edited `Event`
+
+The syntax used to call this command is as follows: `editContactEvent PERSON_INDEX EVENT_INDEX [d/DESCRIPTION] [ts/NEW_START_DATE_TIME][te/NEW_END_DATE_TIME]`, with the `START_DATE_TIME` and `END_DATE_TIME` in the `yyyy-MM-dd HH:mm` format. If any of the fields are missing or if the formatting is incorrect, an error message will be thrown along with usage instructions on the correct formatting.
+
+Given below is an example of how the editContactEvent function works at each step. We will simulate a user using the editContactEvent function to sort UniMate contacts by name in descending order.
+
+Step 1. The user executes `editContactEvent 1 1 d/CS2103 meeting ts/2023-11-11 10:00 te/2023-11-11 12:00` to reschedule the meeting with his CS2103 module group mate. The input is passed into `UniMateParser` which then parses it with the `EditContactEventCommandParser`.
+
+Step 2. The `EditContactEventCommandParser` parses the input and first checks for arguments provided. If the arguments are empty, invalid or in the wrong format, a helper message will appear to allow the user to reference the sample run case. The arguments are then matched by delimiters `d/`, `ts/` amd `te/` to determine the fields to be edited. If certain fields are empty (for example, the user just wants to change the time start and time end of the meeting), the event description from the edited event will be retained when `EditContactEventCommand#execute` is executed in step 4.
+
+Step 3. `EditContactEventCommandParser` creates a temporary event `EditEventDescriptor` as well as an array consisting of two elements, that is the PERSON_INDEX and EVENT_INDEX to be parsed. The `EditContactEventCommandParser` finally returns a newly created `EditContactEventCommand` consisting of the array and the temporary event.
+
+Step 4. `EditContactEventCommand#execute` is called. In this method, a new person object is created with the same attributes except an updated calendar with the updated event instead. `model#setPerson` is called to replace the target person with the new person. The observable list is then refreshed to show the new calendar event.
+
+Step 5. The success message is returned as a `CommandResult` and displayed on the GUI result display panel.
+
+Here's a sequence diagram to summarise the steps above:
+
+
+
+**Design considerations**
+
+* The design of the `EditContactEvent` command is dependent on the structure of the `CalendarStorage` object. Should the structure of how the Calendar objects are stored change, a new implementation will be required for the command.
+//@@author
+
### \[Proposed\] Undo/redo feature
#### Proposed Implementation
@@ -170,67 +584,76 @@ Given below is an example usage scenario and how the undo/redo mechanism behaves
Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state.
-
+
Step 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 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`.
-
+
+
+
-
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`.
+**Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`.
-
+
Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state.
-
+
-
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather
+
+
+
+**Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather
than attempting to perform the undo.
-
+
The following sequence diagram shows how the undo operation works:
-
+
+
+
-
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
+**Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
-
+
The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state.
-
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.
+
-
+**Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.
+
+
Step 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 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.
-
+
The following activity diagram summarizes what happens when a user executes a new command:
-
+
#### Design considerations:
**Aspect: How undo & redo executes:**
* **Alternative 1 (current choice):** Saves the entire address book.
- * Pros: Easy to implement.
- * Cons: May have performance issues in terms of memory usage.
+ * Pros: Easy to implement.
+ * Cons: May have performance issues in terms of memory usage.
* **Alternative 2:** Individual command knows how to undo/redo by
itself.
- * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted).
- * Cons: We must ensure that the implementation of each individual command are correct.
+ * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted).
+ * Cons: We must ensure that the implementation of each individual command are correct.
_{more aspects and alternatives to be added}_
@@ -257,71 +680,493 @@ _{Explain here how the data archiving feature will be implemented}_
**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
+* is an NUS student
+* has a need to organize and coordinate schedules
+
**Value proposition**: manage contacts faster than a typical mouse/GUI driven app
+//@@author lihongguang00
+| Priority | As a … | I want to … | So that I can… |
+| -------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
+| `* * *` | NUS student | search for the contacts of other students within my university | contact them for group projects |
+| `* * *` | NUS student | search for a name in the contact | easily find the person’s contact |
+| `* * *` | NUS student | add contacts into my address book easily | retrieve the saved contact |
+| `* * *` | NUS student living on campus | save the contacts of my neighbors | contact them in case of any emergencies |
+| `* * *` | NUS student living on campus | save the addresses of my neighbours | locate them easily when necessary |
+| `* * *` | NUS student in multiple CCAs | filter my contacts by tags to identify all people in a group | find the relevant contacts in a certain group quickly |
+| `* * *` | student staying on campus | label multiple tags to my contacts | locate my friends taking the same module and staying in the same campus residence as me |
+| `* * *` | user that is familiar with the keyboard | use the keyboard to type commands in the applications | access the features of the application |
+| `* * *` | user with bad memory | save a short description of my contact | identify my contacts better |
+| `* * *` | visual-reliant user | save a photo of the person into my contacts | quickly recognise and find them |
+| `* * *` | non-tech-savvy user | use the help feature of the app | navigate about the app easily |
+| `* *` | NUS student | sort all contacts by name | easily find a person's contact |
+| `* *` | NUS student | sort all contacts by name in reverse | easily find a person's contact |
+| `* *` | NUS student | sort all contacts by email | easily find a person's contact |
+| `* *` | NUS student | sort all contacts by email in reverse | easily find a person's contact |
+| `* *` | NUS student | sort all contacts by address | easily find a person's contact |
+| `* *` | NUS student | sort all contacts by address in reverse | easily find a person's contact |
+| `* *` | NUS student | sort all contacts by phone | easily find a person's contact |
+| `* *` | NUS student | sort all contacts by phone in reverse | easily find a person's contact |
+| `* *` | NUS student | import the NUS calendar into the application | view all academic commitments more conveniently |
+| `* *` | NUS Student | compare timetables/calendars with my peers easily | plan meetings more conveniently |
+| `* *` | NUS student | allocate tasks and responsibilities within a project or CCA group | tasks can be done efficiently |
+| `* *` | NUS Student in multiple CCAs | group my contacts | identify which group my contacts belong to |
+| `* *` | NUS student doing a group project | export my calendar in my application | send it to my teammates to coordinate meeting times |
+| `* *` | forgetful user | view a password hint | I can recall my password when I forget it |
+| `* *` | forgetful student | be reminded of upcoming assignments | prioritize which assignment to work on first |
+| `* *` | forgetful student | be reminded of upcoming examinations | prioritize which exam module to study on first |
+| `* *` | forgetful student | be reminded of upcoming projects | prioritize which project to work on first |
+| `* *` | forgetful student | be reminded of upcoming tutorials | so that I can prioritize which tutorial to complete first |
+| `* *` | security conscious user | password-protect my appliation | prevent others from accessing my application profile easily |
+| `* *` | data conscious user | backup my data | so that I can recover it in the case my data is corrupted |
+| `* *` | user who cares about user experience | change the color of my user interface | modify the interface to my liking |
+| `* *` | user with a strong urge for aesthetics | interact with a clean user interface | feel at ease when using the application |
+| `* *` | non-tech-savvy user that does not know how to use terminal commands | use buttons around the application to navigate around the application easily |
+| `*` | student who prefers using a tablet | I can use the application on my tablet | access contacts easily on my tablet |
+| `*` | student who prefers mobile devices | I can use the application on my mobile phone | sync my contacts with those in the application |
+| `*` | student in a group project | send a QR code of my application contact details | have my teammates add my contact details on the application much faster |
+| `*` | user who prefers cloud storage | sync the contacts in my application with Google Contacts | save it in cloud-based storage |
+| `*` | user accustomed to a PC | view all commands at once | explore features quickly |
+| `*` | user with a tendency to open many applications at once | have the application time out and exit | ensure my computer would not be cluttered by too many applications |
+
+_{More to be added}_
+//@@author nicrandomlee
+### Use cases
-### User stories
-Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*`
+(For all use cases below, the **System** is the `UniMate` and the **Actor** is the `user`, unless specified otherwise)
-| 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 |
+**Use case: UC1 List persons**
-*{More to be added}*
-### Use cases
+**MSS**
+
-(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise)
+1. User requests to list persons
+2. UniMate shows a list of persons
+
+Use case ends.
+
+
+**Extensions**
+
+
+* 2a. The list is empty.
+
+
+* 2b. UniMate displays a message to the user.
+
+
+Use case ends.
+
+
+**Use case: UC2 Delete a person**
-**Use case: Delete a person**
**MSS**
-1. User requests to list persons
-2. AddressBook shows a list of persons
-3. User requests to delete a specific person in the list
-4. AddressBook deletes the person
+
+1. User requests to list persons (UC1)
+2. User requests to delete a specific person in the list
+3. UniMate deletes the person
+
+
+Use case ends.
+
+
+* 3a. The given index is invalid.
+
+
+* 3a1. UniMate shows an error message.
+
+
+ Use case resumes at step 2.
+
+
+**Use case: UC3 Add a person**
+
+
+
+
+**MSS**
+
+
+
+
+1. User requests to add in a person into the UniMate
+2. UniMate adds the person
+3. UniMate displays a success message
+
+
+Use case ends.
+
+**Extensions**
+
+
+* 1a. The given format is wrong
+
+ * 1a1. UniMate shows an error message
+
+
+Use case ends.
+
+
+* 1b. Person is already in UniMate
+
+
+ Use case ends.
+
+//@@author Fallman2
+
+**Use case: UC4 Clear All Entries**
+
+
+**MSS**
+
+
+1. User requests to clear all entries in UniMate
+2. UniMate deletes all entries
+
+
+ Use Case Ends.
+
+**Extensions**
+
+
+* 1a. UniMate has no data
+
+
+ Use Case Ends
+
+
+**Use case: UC5 Edit persons**
+
+
+**MSS**
+
+
+1. Use requests to list all persons(UC1).
+2. User requests to edit person at a specified index with new parameters.
+3. UniMate updates the person at specified index with new parameters.
+ Use case ends.
+
+
+**Extensions**
+
+
+* 2a. The given index is invalid.
+ * 2a1. UniMate displays an error message.
+ Use case ends.
+* 2b. The given parameters are invalid.
+ * 2b1. UniMate displays an error message.
+ Use case ends.
+
+
+**Use case: UC6 Exit application**
+
+
+**MSS**
+
+
+1. User requests to exit the application.
+2. UniMate closes the application.
+ Use case ends.
+
+
+**Use case: UC7 Search by name**
+
+
+**MSS**
+
+
+1. User requests to search persons by specified keywords
+2. UniMate displays persons with names that match keywords
+ Use case ends.
+
+
+//@@author junhonglow
+**Use case: UC8 View events**
+
+
+**MSS**
+
+
+1. User requests to view events.
+2. UniMate displays all events.
+
+
+Use Case ends.
+
+
+**Extensions**
+* 2a. There are no events recorded.
+ * 2a1. UniMate displays a message indicating that there are no events.
Use case ends.
+
+**Use case: UC9 Add an event**
+
+
+**MSS**
+
+
+1. User requests to add an event.
+2. UniMate adds the event.
+
+
+Use case ends.
+
+
**Extensions**
+* 2a. UniMate detects a conflict with an existing event.
+ * 2a1. UniMate shows conflicted timings and requests to modify one of the timings.
+ * 2a2. User modifies the timing and submits the new timings.
+ * Steps 2a1 and 2a2 are repeated until there are no conflicts in timings.
-* 2a. The list is empty.
+ Use case ends.
+
+
+**Use case: UC10 Delete an event**
+
+
+**MSS**
+
+
+1. User requests to view events.(UC 8)
+2. User requests to delete an event.
+3. UniMate deletes the event.
+
+
+Use case ends.
+
+
+**Extensions**
+* 2a. Event does not exist.
+ * 2a1. UniMate shows an error message.
+
+
+ Use case ends.
+
+
+**Use case: UC11 Edit an event**
+
+
+**MSS**
+
+
+1. User requests to view events. (UC 8)
+2. User requests to edit an event.
+3. Unimate edits the event.
+
+
+**Extensions**
+* 2a. Event does not exist.
+ * 2a1. UniMate shows an error message.
+
+
+ Use case ends.
+
+
+* 3a. UniMate detects a conflict with an existing event.
+ * 3a1. UniMate shows conflicted timings and requests to modify one of the timings.
+ * 3a2. User modifies the timing and submits the new timings.
+ * Steps 3a1 and 3a2 are repeated until there are no conflicts in timings.
+
+
+ Use case ends.
+
+//@@author Fallman2
+
+**Use case: UC12 Filter by fields.**
+
+
+**MSS**
+
+
+1. User requests to list persons with specific words in specific fields.
+2. UniMate displays persons matching all provided keywords in specified fields and the number of people matching all
+ specified fields.
+
+ Use case ends.
+
+
+**Extensions**
+
+
+* 2a. There are no persons matching the specified keywords.
+ * UniMate displays a message that 0 people have been displayed.
+
+ Use case ends.
+* 2b. All indicated fields are left empty instead.
+ * UniMate displays all available contacts.
+
+ Use case ends.
+
+//@@author nicrandomlee
+
+**Use case: UC13 Sort by fields.**
+
+**MSS**
+
+1. User requests to sort persons
+2. UniMate displays persons in specified order and by specified parameter
+
+ Use case ends.
+
+**Extensions**
+
+- 1a. User uses the wrong delimiter or makes a spelling mistake, or provides incorrect number of arguments
+ - UniMate displays a message to show a helper message outlining the correct syntax and available sort options
+
+//@@author
+
+//@@author lihongguang00
+
+**Use case: UC14 Compare calendars**
+
+**MSS**
+1. User requests to compare calendars
+2. User inputs and confirms the indices of the contacts they want to compare their calendars with
+3. UniMate displays the calendar showing the time periods where both the contacts and the user are free
+
+Use case ends.
+
+**Extensions**
+
+- 2a. User inputs and confirms the tags of the contacts to identify who they want to compare their calendars with.
+
+ Use case resumes from step 3
+
+- 2b. User inputs the invalid arguments for the command.
+ - UniMate displays the user's calendar only, ignoring all the invalid arguments
+ - Use case ends.
+ -
+//@@author
+
+//@@author Fallman2
+
+**Use case: UC15 Adding a Task.**
+
+**MSS**
+
+1. User requests to add a Task.
+2. UniMate adds the task.
Use case ends.
-* 3a. The given index is invalid.
+**Extensions**
+
+* 1a. User leaves the description field empty.
+ * UniMate displays a message to indicate that the description cannot be empty.
+
+ Use case ends.
+* 1b. User attempts to create a Task that is identical to another Task in the Task Manager.
+ * UniMate displays a message to indicate that there is already a duplicate task present.
+
+ Use case ends.
+
+**Use case: UC16 Viewing the task list.**
+
+**MSS**
+
+1. User requests to switch the list from the event list to the task list.
+2. UniMate switches the list.
+
+ Use case ends.
+
+**Extensions**
+
+* 1a. The list displayed is already the task list.
+ * UniMate displays the event list instead.
+
+ Use case ends.
+
+**Use case: UC17 Deleting a task.**
+
+**MSS**
+
+1. User switches the event list to the task list to view it (UC17).
+2. User requests to delete a task.
+3. UniMate deletes the task from the TaskManager.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. There are no tasks to delete .
+ * UniMate displays an error message indicating that the index indicated is invalid.
+
+ Use case ends.
+
+**Use case: UC18 Sorting Tasks.**
+
+**MSS**
+
+1. User switches the event list to the task list to view it (UC17).
+2. User requests to sort the tasks by description.
+3. UniMate sorts the tasks displayed by description alphabetically.
+
+ Use case ends.
+
+**Extensions**
+
+* 2a. User requests to sort the tasks by deadline instead.
+ * UniMate sorts the tasks displayed by deadline from earliest to latest.
+
+ Use case ends.
+
+**Use case: UC19 Clearing Events within a Time Range.**
+
+**MSS**
+
+1. User requests to clear all events within a time range.
+2. UniMate deletes all events that overlap with the time range.
+
+ Use case ends.
- * 3a1. AddressBook shows an error message.
+**Extensions**
+
+* 1a. Time range provided has a start time after the end time.
+ * UniMate displays an error message indicating that the start time must be before the end time.
+
+ Use case ends.
+* 1b. There are no events within the time range.
+ * UniMate displays an error message indicating that there are no events within the indicated times.
- Use case resumes at step 2.
+ Use case ends.
-*{More to be added}*
+//@@author
### 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.
+Project Constraints:
+1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed.
+2. The product is offered as an offline application.
+3. System should be beginner-friendly and easily picked up by a freshman at NUS with just the application's website.
+4. Should be able to hold up to 200 events without a noticeable sluggishness in performance for typical usage.
+5. Calendar should have the current week's event schedule displayed.
+6. Calendar should be able to check if there are any event timing conflicts within 1 second.
+7. 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, except viewing contact events as a pop-up.
+8. System should be beginner-friendly and easily picked up by a freshman at NUS with just the application's website.
+
-*{More to be added}*
+Process Requirements:
+1. The product is expected to adhere to the set Milestones.
+2. User can only execute actions using at most 1 command at a time.
### Glossary
* **Mainstream OS**: Windows, Linux, Unix, OS-X
-* **Private contact detail**: A contact detail that is not meant to be shared with others
+* **Event**: An activity to be marked on the calendar with a specified time frame, with a description, some start date and time, and some end date and time.
+* **Task**: An activity that has a description, and an optional deadline date and time.
--------------------------------------------------------------------------------------------------------------------
@@ -329,49 +1174,398 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli
Given below are instructions to test the app manually.
-
:information_source: **Note:** These instructions only provide a starting point for testers to work on;
+
+
+**Note:** These instructions only provide a starting point for testers to work on;
testers are expected to do more *exploratory* testing.
-
+
### Launch and shutdown
1. Initial launch
+ 1. Download the jar file and copy into an empty folder
- 1. Download the jar file and copy into an empty folder
-
- 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum.
+ 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum.
1. Saving window preferences
- 1. Resize the window to an optimum size. Move the window to a different location. Close the window.
+ 1. Resize the window to an optimum size. Move the window to a different location. Close the window.
- 1. Re-launch the app by double-clicking the jar file.
- Expected: The most recent window size and location is retained.
+ 2. Re-launch the app by double-clicking the jar file.
+ Expected: The most recent window size and location is retained.
-1. _{ more test cases … }_
+3. Shutting down via command
+
+1. Launch the app.
+
+2. Type `exit` into the input box and press `ENTER`.
+
+4. Shutting down via `file`
+
+1. Launch the app.
+
+2. Click on `file` in the top menu bar.
+
+3. Click on `exit` in the dropdown menu.
### 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` command. Multiple persons in the list.
- 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated.
+ 2. Test case: `delete 1`
+ Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated.
- 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same.
+ 3. Test case: `delete 0`
+ Expected: No person is deleted. Error details shown in the status message. Status bar remains the same.
- 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous.
+ 4. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous.
-1. _{ more test cases … }_
+### Filtering all persons while all persons are being shown.
-### Saving data
+Prerequisites: List all persons using the `list` command. Multiple persons in the list.
+
+1Test case: `filter n/ p/ e/`
+Expected: All persons displayed.
+
+1. Test case: `filter n/o`
+ Expected: All persons with an "o" in their name displayed.
+
+2. Test case: `filter n/o p/8 e/@ t/CS t/10`
+ Expected: All persons with an "o" in their name, an "8" in their phone number, a tag that contains "CS" and a tag
+ that contains "10" displayed.
+
+3. Test case: `filter`
+ Expected: Error message displayed that states that the command format is invalid. all persons shown.
+
+### Sorting all persons while all persons are shown.
+
+Prerequisites: List all persons using the `list` command. Multiple persons in the list.
+
+1. Test case: `sort /byname`
+ Expected: All persons sorted alphabetically by name.
+
+2. Test case: `sort /byname /reverse`
+ Expected: All persons sorted in reverse alphabetical order by name.
+
+3. Test case: `sort /byemail`
+ Expected: All persons sorted by email.
+
+4. Test case: `sort /byphone`
+ Expected: All persons sorted by phone number.
+
+5. Test case: `sort /byaddress`
+ Expected: All persons sorted by address.
+
+6. Test case: `sort`
+ Expected: Error message displayed indicating invalid command format.
+
+### Adding an event
+
+Prerequisite: Empty event list.
-1. Dealing with missing/corrupted data files
+1. Test case: `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Expected: New event added with description "Cry about deadlines", start date and time "2023-01-01 00:01" and end date
+ and time "2023-12-31 23:59".
+2. Test case: `addEvent d/Laugh about deadlines ts/2023-01-02 00:01 te/2023-01-01 23:59`
+ Expected: Error message displayed indicating invalid event period.
+3. Test case: `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then, re-enter: `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Expected: Error message displayed indicating conflicting event timing.
- 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_
+### Deleting an event
-1. _{ more test cases … }_
+Prerequisite: Empty event list.
+
+1. Test case: Enter `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `deleteEvent 2023-02-02 00:01`
+ Expected: Event is created and then successfully deleted
+2. Test case: `deleteEvent 2023-02-02 00:01`
+ Expected: Error message displayed indicating no event found at specified time.
+
+### Clearing events within time range
+
+Prerequisite: Empty event list.
+
+1. Test case: Enter `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-01-02 23:59`
+ Enter `addEvent d/Cry about deadlines ts/2023-01-03 00:01 te/2023-01-04 23:59`
+ Enter `clearEvents ts/2023-01-01 00:02 te/2023-01-04 23:00`
+ Expected: All events in Calendar deleted.
+2. Test case: `clearEvents ts/2023-01-01 00:02 te/2023-01-04 23:00`
+ Expected: Error message displayed indicating no events found in time range.
+3. Test case: Enter `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-01-02 23:59`
+ Enter `clearEvents ts/2023-01-01 00:02 te/2023-01-04 23:00`
+ Expected: All events in Calendar deleted.
+
+### Switching the bottom list display
+
+Prerequisite: Event list currently being displayed.
+
+1. Test case: `switchList`
+ Expected: Task list is displayed.
+2. Test case: `switchList 123`
+ Expected: Task list is displayed.
+
+### Adding an event for another person
+
+Prerequisite: 1 person in AddressBook, person currently has no events.
+
+1. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Expected: New event added with description "Cry about deadlines", start date and time "2023-01-01 00:01" and end date
+ and time "2023-12-31 23:59".
+2. Test case: `addContactEvent 1 d/Laugh about deadlines ts/2023-01-02 00:01 te/2023-01-01 23:59`
+ Expected: Error message displayed indicating invalid event period.
+3. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then, re-enter: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Expected: Error message displayed indicating conflicting event timing.
+
+### Deleting an event for another person
+
+Prerequisite: 1 person in AddressBook, person currently has no events.
+
+1. Test case: Enter `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `deleteContactEvent 1 2023-02-02 00:01`
+ Expected: Event is created and then successfully deleted
+2. Test case: `deleteContactEvent 1 2023-02-02 00:01`
+ Expected: Error message displayed indicating no event found at specified time.
+
+### Editing an event for another person
+
+Prerequisite: 1 person in AddressBook, person currently has no events.
+
+1. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `editContactEvent 1 1 d/Edited Description`
+ Expected: A new event is added for the person in the AddressBook, then the description of that contact is edited to be "Edited Description".
+2. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `editContactEvent 1 1 ts/2023-01-01 00:20`
+ Expected: A new event is added for the person in the AddressBook, then the start time is edited to be 2023-01-01 00:20
+3. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `editContactEvent 1 1 ts/2023-01-03 00:20`
+ Expected: Error message is displayed that indicates that the start date should be before the end date.
+4. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `editContactEvent 1 1 te/2023-01-04 00:20`
+ Expected: A new event is added for the person in the AddressBook, then the end time is edited to be 2023-01-04 00:20.
+5. Test case: `addContactEvent 1 d/Cry about deadlines ts/2023-01-02 00:01 te/2023-12-31 23:59`
+ Then enter: `editContactEvent 1 1 t3/2023-01-01 00:20`
+ Expected: Error message is displayed that indicates that the start date should be before the end date.
+6. Test case: `addContactEvent`
+ Expected: Error message displayed indicating that the command format is invalid.
+
+### Viewing events of another person
+
+Prerequisite: 1 person in AddressBook, person currently has no events.
+
+1. Test case: `viewContactEvents 1`
+ Expected: Empty event list is displayed in a pop-up
+2. Test case: Enter `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+ Then enter: `viewContactEvents 1`
+ Expected: Event list displayed in a pop-up with 1 event listed.
+3. Test case: `viewContactEvents 2`
+ Expected: Error message displayed indicating that the index indicated is invalid.
+4. Test case: `viewContactEvents`
+ Expected: Error message displayed indicating that the command format is invalid.
+
+### Comparing Calendars of multiple people
+
+Prerequisite: 3 persons in AddressBook.
+
+1. Test case: `compareCalendars`
+ Expected: User's calendar is shown.
+2. Test case: `compareCalendars 1 3`
+ Expected: Calendar comparison is shown in a pop-up with the comparison being between the user, the person of index 1 and the person of index 3.
+3. Test case: `compareCalendars 4`
+ Expected: Error message displayed indicating that the index indicated is invalid.
+
+### Comparing Calendars of multiple people via tags
+
+Prerequisite: 3 persons in AddressBook, 2 persons have the tag "CS2103".
+
+1. Test case: `compareGroupCalendars`
+ Expected: User's calendar is shown.
+2. Test case: `compareGroupCalendars CS2103`
+ Expected: Calendar comparison is shown in a pop-up with the comparison being between the user and the two people with the "CS2103" tag.
+
+### Adding a task
+
+Prerequisite: No tasks in the task list.
+
+1. Test case: `addTask d/Hydrate te/2023-11-13 22:30`
+ Expected: A new task is added with description "Hydrate" and deadline of 2023-11-13 22:30.
+2. Test case: `addTask d/Hydrate`
+ Expected: A new task is added with description "Hydrate" and no deadline.
+3. Test case: Enter `addTask d/Hydrate te/2023-11-13 22:30`
+ Then enter: `addTask d/Hydrate te/2023-11-13 22:30`
+ Expected: Error message displayed indicating that there is a conflicting task.
+
+### Deleting a task
+
+Prerequisite: No tasks in the task list.
+
+1. Test case: Enter `addTask d/Hydrate te/2023-11-13 22:30`
+ Then enter: `deleteTask 1`
+ Expected: Task added and then deleted successfully.
+2. Test case: `deleteTask 1`
+ Expected: Error message displayed indicating that the index indicated is invalid.
+
+### Sorting Tasks
+
+Prerequisite: Multiple tasks in the task list.
+
+1. Test case: `sortTasks DESCRIPTION`
+ Expected: Tasks are sorted alphabetically by description.
+2. Test case: `sortTasks DEADLINE`
+ Expected: Tasks are sorted by deadline.
+3. Test case: `sortTasks 1askdj`
+ Expected: Error message displayed indicating that the sorting order indicated is invalid.
+
+### Saving data
+Prerequisites: Have all three json data files present.
+ * addressbook.json
+ * calendar.json
+ * taskmanager.json
+
+
+Some sample storage json files for the tests can be found in the link [here](https://github.com/AY2324S1-CS2103-F13-4/tp/tree/master/src/test/data/ManualTestingSampleStorageFiles).
+Rename the files to their respective data files and replace the original files (if any) found in the original directory.
+1. Test case: Navigate to the addressbook.json file and delete the file.
+ Expected: The AddressBook (Contact List) in UniMate should be populated with sample data.
+
+2. Test case: Single or multiple deletion of the different data files.
+ Expected: Similar to previous.
+
+3. Test case: Navigate to the addressbook.json file and edit any of the fields to become an invalid field.
+ Example: Modify the events field of a contact to the following.
+ {
+ "persons" : [ {
+ "name" : "Alex Yeoh",
+ "phone" : "87438807",
+ "email" : "alexyeoh@example.com",
+ "address" : "Blk 30 Geylang Street 29, #06-40",
+ "tags" : [ "friends" ],
+ "events" : [ {
+ "description" : "Nap",
+ "eventPeriod" : "Invalid Period - 2024-01-03 18:00"
+ } ]
+ }
+ Expected: The AddressBook (Contact List) in UniMate should be blank as UniMate will discard all data.
+ Note: In this scenario, the event period is invalid.
+
+4. Test case: Single or multiple corruption of the different data files.
+ Expected: Similar to previous.
+
+5. Test case: Navigate to the addressbook.json file and duplicate a person in the file.
+ Example:
+ {
+ "persons" : [ {
+ "name" : "Bernice Yu",
+ "phone" : "99272758",
+ "email" : "berniceyu@example.com",
+ "address" : "Blk 30 Lorong 3 Serangoon Gardens, #07-18",
+ "tags" : [ "colleagues", "friends" ],
+ "events" : [ ]
+ }, {
+ "name" : "Bernice Yu",
+ "phone" : "99272758",
+ "email" : "berniceyu@example.com",
+ "address" : "Blk 30 Lorong 3 Serangoon Gardens, #07-18",
+ "tags" : [ "colleagues", "friends" ],
+ "events" : [ ]
+ } ]
+ }
+ Expected: The AddressBook (Contact List) in UniMate should be blank as UniMate will discard all data.
+
+6. Test case: Duplication of the fields in the different data files.
+ Expected: Similar to previous.
+
+7. Test case: Other invalid storage formats: Not a JSON format.
+ Expected: Similar to previous.
+
+## **Appendix: Effort**
+### Main Difficulty: Building User Calendar
+This part was especially difficult because we wanted to conform to the structure of the base `AB3` code, knowing it is a brownfield project.
+Thus, we wanted to ensure the abstraction granularity of the classes matched that of `AB3` (e.g. Separate classes for every single attribute of `Person`),
+which explains why the `UniMateCalendar` class is so granular, as seen in the [model class diagram](#model-component).
+
+Furthermore, we knew from the beginning that we wanted to allow users to compare their `Calendar` with that of their contacts.
+Therefore, from the start, we had to write code that would make this feature possible.
+
+The UI for the calendar was also challenging because nobody in the group had experience in desktop application UI development and JavaFx.
+Understanding JavaFx, before even implementing it in the creation of new GUI elements, required a few hours and much trial and error.
+It was also challenging to implement GUI components while ensuring that all elements that were already previously implemented worked as well.
+
+Lastly, the feature where the calendar would "grow" and "shrink" according to the events that the user have in real time
+took a lot of careful problem-solving to avoid breaking abstraction barrier and introducing cyclic dependencies, since
+we had to establish a line of communication between the `UniMateCalendar` of the `Model` interface and the `UI` interface,
+and the `Observable` and `Observer` interfaces were only introduced later into the module, by which we had already implemented
+this feature.
+
+### Other Difficulty: Contact Calendar
+Integrating the calendar into the contacts in the AddressBook was challenging, but was slightly easier as we could just reuse the
+code that we have written for the `UniMateCalendar`. However, we still had to learn how to make the calendar of the contacts show up
+in a new window rather than in the main application GUI.
+
+### Other Difficulty: Task Manager/Event List
+For the Task Manager, the main difficulty lies in following the structure of the `AB3` code.
+While the implementation of the TaskManager was not complicated, we wanted to properly abstract the different classes from each other similarly to the
+structure of AB3. Thus, the implementation required a large number of lines of code and methods, all of which required even more JUnit tests, multiplying
+the number of hours spent on implementing the TaskManager. However, the implementation of the GUI for the TaskManager did not take as long as that
+of the Calendar as the TaskManager used the same area of the GUI as the event list. That being said, given that no one in our team had experience working
+on a GUI, especially with JavaFx, a few hours had to be spent in researching what would be the best way to carry out the switching of the lists from one to the other.
+
+Additionally, for both `Task List` and `Event List`, making the list update in real time when new `Task`/`Event` are added was also difficult. When implementing
+the GUI, we had wanted to ensure that commands executed in the main window would reflect appropriately in both the user's own task and event lists but also
+for the pop-up event list of individual persons in the AddressBook. This proved to be another challenge as we would encounter several bugs while implementing
+this feature. However, we managed to work together as a group to fix most of them.
+
+### Achievements:
+- Calendar UI that will update in real time when user adds/deletes event
+- Working calendar comparison feature
+- We were able to integrate 2 separate and very different components, `Task manager` and `Calendar`, successfully into the base `AB3` code
+- Live updates for the `Task List` and `Event List`
+
+## **Appendix: Planned Enhancements**
+
+### Better Confirmation for ClearEventsCommand
+Currently, for the ClearEventsCommand, the user has to re-enter the command entirely with the confirmation text in order
+to confirm the deletion of the events. This implementation has an advantage of being able to bypass the confirmation
+check if the user wishes to delete the events more quickly but is otherwise clumsier to use. One way we tried to
+circumvent this is by displaying the message with the confirmation in the result shown but this would not be as convenient
+for users who do not wish to use their mouse. A better implementation would be to have the confirmation only require the
+user to key in Y or N to confirm or deny the deletion, allowing the events to still be deleted quickly for users who wish
+to do so but also allowing for less text to be keyed in to confirm.
+
+### More Ways to Sort Tasks
+Currently, there are only two ways to sort tasks. This can cause some tasks to be buried under others if they are not
+prioritized in either sorting method. Being able to sort tasks by proximity to the current date or being able to reverse
+the order of sorting of the tasks may help in making all tasks visible to the user so as to ensure that all tasks are
+completed on time.
+
+### Allow user to view beyond the current week restriction for the calendar
+Currently, the calendar for the user and contacts is restricted to viewing the current week's events. Therefore, the
+user has no means of navigating looking at their events for other weeks other than the current one. This might impede
+the user experience as the user might want to check their timetable for some timeframe in the near future (e.g. proceeding week).
+Therefore, allowing the user to view their events for any week beyond just the current one might improve user experience,
+reducing the dependency the user has on the `Event List` to view events beyond this chronological restriction.
+
+### Allow users to compare calendars with contacts beyond the current week
+Currently, the user can only compare calendars with their contacts for the current week. Therefore, if the user wants to
+schedule a meeting in a week beyond the current one, they might have difficulty as they might not be able to compare
+their calendars with their contacts for that specific week. Therefore, allowing the user to navigate beyond the current
+week restriction for the compare calendar feature will be hugely beneficial to the user.
+
+### Allow users to add single day events without typing the date twice
+Currently, when the user wants to add an `Event` into their `Calendar`, they have to type the date of the event twice
+(once in start date and time and another in end date and time). For single day events, this can be inconvenient. Hence,
+we hope to introduce a command that will allow users to add a single day event without typing the date twice.
+
+### Allow users to add recurring events
+Currently, our `Calendar` only supports adding of singular events. We hope to allow users to add recurring events in the future
+to more accurately simulate the events that the user might have in a University context, where they might have recurring classes
+every week.
+
+### Allow users to import .ics format files
+Currently, UniMate has no support for importing of external files. Allowing users to import .ics files, which is a common export
+format of other calendar platforms, will allow UniMate to integrate more seamlessly with other calendar applications.
diff --git a/docs/Documentation.md b/docs/Documentation.md
index 3e68ea364e7..082e652d947 100644
--- a/docs/Documentation.md
+++ b/docs/Documentation.md
@@ -1,29 +1,21 @@
---
-layout: page
-title: Documentation guide
+ layout: default.md
+ title: "Documentation guide"
+ pageNav: 3
---
-**Setting up and maintaining the project website:**
-
-* We use [**Jekyll**](https://jekyllrb.com/) to manage documentation.
-* The `docs/` folder is used for documentation.
-* To learn how set it up and maintain the project website, follow the guide [_[se-edu/guides] **Using Jekyll for project documentation**_](https://se-education.org/guides/tutorials/jekyll.html).
-* Note these points when adapting the documentation to a different project/product:
- * The 'Site-wide settings' section of the page linked above has information on how to update site-wide elements such as the top navigation bar.
- * :bulb: In addition to updating content files, you might have to update the config files `docs\_config.yml` and `docs\_sass\minima\_base.scss` (which contains a reference to `AB-3` that comes into play when converting documentation pages to PDF format).
-* If you are using Intellij for editing documentation files, you can consider enabling 'soft wrapping' for `*.md` files, as explained in [_[se-edu/guides] **Intellij IDEA: Useful settings**_](https://se-education.org/guides/tutorials/intellijUsefulSettings.html#enabling-soft-wrapping)
+# Documentation Guide
+* We use [**MarkBind**](https://markbind.org/) to manage documentation.
+* The `docs/` folder contains the source files for the documentation website.
+* To learn how set it up and maintain the project website, follow the guide [[se-edu/guides] Working with Forked MarkBind sites](https://se-education.org/guides/tutorials/markbind-forked-sites.html) for project documentation.
**Style guidance:**
* Follow the [**_Google developer documentation style guide_**](https://developers.google.com/style).
+* Also relevant is the [_se-edu/guides **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html).
-* Also relevant is the [_[se-edu/guides] **Markdown coding standard**_](https://se-education.org/guides/conventions/markdown.html)
-
-**Diagrams:**
-
-* See the [_[se-edu/guides] **Using PlantUML**_](https://se-education.org/guides/tutorials/plantUml.html)
-**Converting a document to the PDF format:**
+**Converting to PDF**
-* See the guide [_[se-edu/guides] **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html)
+* See the guide [_se-edu/guides **Saving web documents as PDF files**_](https://se-education.org/guides/tutorials/savingPdf.html).
diff --git a/docs/Gemfile b/docs/Gemfile
deleted file mode 100644
index c8385d85874..00000000000
--- a/docs/Gemfile
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-source "https://rubygems.org"
-
-git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
-
-gem 'jekyll'
-gem 'github-pages', group: :jekyll_plugins
-gem 'wdm', '~> 0.1.0' if Gem.win_platform?
-gem 'webrick'
diff --git a/docs/Logging.md b/docs/Logging.md
index 5e4fb9bc217..589644ad5c6 100644
--- a/docs/Logging.md
+++ b/docs/Logging.md
@@ -1,8 +1,10 @@
---
-layout: page
-title: Logging guide
+ layout: default.md
+ title: "Logging guide"
---
+# Logging guide
+
* We are using `java.util.logging` package for logging.
* The `LogsCenter` class is used to manage the logging levels and logging destinations.
* The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level.
diff --git a/docs/SettingUp.md b/docs/SettingUp.md
index 275445bd551..03df0295bd2 100644
--- a/docs/SettingUp.md
+++ b/docs/SettingUp.md
@@ -1,27 +1,32 @@
---
-layout: page
-title: Setting up and getting started
+ layout: default.md
+ title: "Setting up and getting started"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+# Setting up and getting started
+
+
--------------------------------------------------------------------------------------------------------------------
## Setting up the project in your computer
-
:exclamation: **Caution:**
+
+**Caution:**
Follow the steps in the following guide precisely. Things will not work out if you deviate in some steps.
-
+
First, **fork** this repo, and **clone** the fork into your computer.
If you plan to use Intellij IDEA (highly recommended):
1. **Configure the JDK**: Follow the guide [_[se-edu/guides] IDEA: Configuring the JDK_](https://se-education.org/guides/tutorials/intellijJdk.html) to to ensure Intellij is configured to use **JDK 11**.
-1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
- :exclamation: Note: Importing a Gradle project is slightly different from importing a normal Java project.
+1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
+
+ Note: Importing a Gradle project is slightly different from importing a normal Java project.
+
1. **Verify the setup**:
1. Run the `seedu.address.Main` and try a few commands.
1. [Run the tests](Testing.md) to ensure they all pass.
@@ -34,10 +39,11 @@ If you plan to use Intellij IDEA (highly recommended):
If using IDEA, follow the guide [_[se-edu/guides] IDEA: Configuring the code style_](https://se-education.org/guides/tutorials/intellijCodeStyle.html) to set up IDEA's coding style to match ours.
-
:bulb: **Tip:**
+
+ **Tip:**
Optionally, you can follow the guide [_[se-edu/guides] Using Checkstyle_](https://se-education.org/guides/tutorials/checkstyle.html) to find how to use the CheckStyle within IDEA e.g., to report problems _as_ you write code.
-
+
1. **Set up CI**
diff --git a/docs/Testing.md b/docs/Testing.md
index 8a99e82438a..78ddc57e670 100644
--- a/docs/Testing.md
+++ b/docs/Testing.md
@@ -1,12 +1,15 @@
---
-layout: page
-title: Testing guide
+ layout: default.md
+ title: "Testing guide"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+# Testing guide
---------------------------------------------------------------------------------------------------------------------
+
+
+
+
## Running tests
@@ -19,8 +22,10 @@ There are two ways to run tests.
* **Method 2: Using Gradle**
* Open a console and run the command `gradlew clean test` (Mac/Linux: `./gradlew clean test`)
-
:link: **Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle.
-
+
+
+**Link**: Read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html) to learn more about using Gradle.
+
--------------------------------------------------------------------------------------------------------------------
diff --git a/docs/UserGuide.md b/docs/UserGuide.md
index 57437026c7b..c7aa9bf39c6 100644
--- a/docs/UserGuide.md
+++ b/docs/UserGuide.md
@@ -1,33 +1,35 @@
---
-layout: page
-title: User Guide
+ layout: default.md
+ title: "User Guide"
+ pageNav: 3
---
-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.
+UniMate is a desktop app for students to **manage contacts** and **manage schedules** 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, UniMate can get your contact management tasks done faster than traditional GUI apps.
-* Table of Contents
-{:toc}
+
+
--------------------------------------------------------------------------------------------------------------------
-## Quick start
+# Quick start
1. Ensure you have Java `11` or above installed in your Computer.
-1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases).
+2. Download the latest `UniMate.jar` from [here](https://github.com/AY2324S1-CS2103-F13-4/tp/releases).
-1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook.
+3. Copy the file to the folder you want to use as the _home folder_ for UniMate.
-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.
- 
+4. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar UniMate.jar` command to run the application.
+ A GUI similar to the below should appear in a few seconds. Note that the app contains some sample data,
+which may differ from that of a fresh download.
+ 
-1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
+5. Type the command in the command box and press 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.
- * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book.
+ * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the AddressBook.
* `delete 3` : Deletes the 3rd contact shown in the current list.
@@ -35,21 +37,109 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo
* `exit` : Exits the app.
-1. Refer to the [Features](#features) below for details of each command.
+6. Refer to the [Features](#features) below for details of each command.
--------------------------------------------------------------------------------------------------------------------
+# Features Overview
-## Features
+UniMate has 3 core features, [**AddressBook**](#main-feature-1-addressbook), [**Calendar System**](#main-feature-2-calendar-system)
+and [**Task Management System**](#main-feature-3-task-management-system). This section serves to
+provide you with an overview of what each component entails and the functionalities supporting them.
-
+
-**:information_source: Notes about the command format:**
+The screenshot above shows a cropped view of UniMate's GUI. **AddressBook** belongs to the [**AddressBook** feature](#main-feature-1-addressbook),
+**CALENDAR** belongs to the [**Calendar System** feature](#main-feature-2-calendar-system) and the **EVENT LIST/TASK LIST**
+portion is shared by both the [**Calendar System** feature](#main-feature-2-calendar-system) and [**Task Management System** feature](#main-feature-3-task-management-system).
+
+>To switch between the [Event List](#sub-feature-2-event-list) and [Task List](#main-feature-3-task-management-system),
+we have a [CLI-based command](#viewing-tasks-switchlist) for that.
+
+## Main feature 1: AddressBook
+
+The AddressBook serves to ease contact management.
+
+Some major CLI-based functionalities to support this feature include:
+- [**Adding**](#adding-a-person-add), [**deleting**](#deleting-a-person-delete) and [**editing**](#editing-a-person-edit) operations to manipulate contacts in the AddressBook
+- [**Finding**](#locating-persons-by-name-find) and [**filtering**](#filtering-persons-by-attribute-filter) operations to search for and isolate contacts of interest
+- [**Sorting**](#sort-persons-sort) operation to reorganise the view of the AddressBook when needed
+
+Other functionalities can be found in the [AddressBook Management subsection found in the Features section](#addressbook-management).
+
+Additionally, each contact in the AddressBook has their own personal calendar,
+which can be accessed simply by double-clicking with the left mouse button on the contact's card in the AddressBook.
+This will create a pop-up displaying the contact's calendar as shown below.
+
+
+
+**Note:** There is currently no CLI-based command to access this feature, but is planned for a future release.
+
+## Main feature 2: Calendar System
+The calendar system seeks to improve the user's schedule planning experience, as well as simplify
+the synchronization of schedules among multiple people.
+
+To achieve this, we have 2 supporting sub-features:
+1. [Calendar](#sub-feature-1-calendar)
+2. [Event List](#sub-feature-2-event-list)
+
+For the calendar system, we work solely with `events`, which differs from `tasks` used in the [task list feature](#main-feature-3-task-management-system).
+- `event` has a `description`, `start date and time` and `end date and time`
+- `task` only has a `description` and an **optional** `end date and time`
+
+### Sub-feature 1: Calendar
+
+The calendar allows the user to plan their timetable and compare it against their contacts' schedules.
+This will facilitate scheduling of meetings and events involving the user and their contacts.
+
+>By default, the calendar's GUI will only display the time period of 8am to 6pm, but will automatically
+extend when events that cannot be fit within this time period are added.
+>
+>Additionally, the GUI for this sub-feature currently only supports viewing of events within the current week.
+Upcoming updates will allow the user to navigate beyond this chronological restriction. In the meantime, users can
+consider using the [Event List](#sub-feature-2-event-list) to view events outside the current week.
+
+For the **user**'s calendar, some major CLI-based functionalities supporting this sub-feature includes:
+- [Adding](#user-calendar-adding-an-event-addevent), [deleting](#user-calendar-deleting-an-event-deleteevent) and [clearing](#user-calendar-deleting-multiple-events-clearevents) operations to manipulate the events in the user's calendar
+
+For the **contact**'s calendar, some major CLI-based functionalities supporting this sub-feature includes:
+- [Adding](#contact-calendar-adding-an-event-to-a-contact-addcontactevent), [deleting](#contact-calendar-deleting-an-event-from-a-contact-deletecontactevent) and [editing](#contact-calendar-edit-contact-calendar-event-editcontactevent) operations to manipulate the events in the contact's calendar
+
+Other useful CLI-based functionalities not exclusive to the user/contact supporting this sub-feature includes:
+- Comparison operations to compare the user's calendar with their contacts' calendars either by [index](#1-comparison-by-index) or [tag](#2-comparison-by-tag)
+
+Other functionalities can be found in the [Calendar System subsection of the Features section](#calendar-system)
+
+### Sub-feature 2: Event List
+
+The event list displays all the events for the user/contact. It serves as an overview of the collective
+events that the user or their contact have.
+
+The event list shown on the main UniMate GUI is the user's event list. To view a specific contact's
+event list, we have a [CLI-based command](#viewing-contact-event-list-viewcontactevents) that will
+display the contact's event list in a popup window.
+
+## Main-feature 3: Task Management System
+
+The task list stores a collection of tasks that the user might have. The primary difference
+between event and task is that a task does not have a starting date and time.
+
+Some useful CLI-based functionalities supporting this feature includes:
+- [Adding](#adding-tasks-addtask) and [deleting](#deleting-tasks-deletetask) operations to manipulate the tasks present in the task list
+- [Sorting](#sorting-tasks-sorttasks) operation to reorganise the view of the task list when needed
+
+--------------------------------------------------------------------------------------------------------------------
+
+# Features
+
+
+
+**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`.
* Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`.
+ e.g. `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`.
* 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.
@@ -60,66 +150,113 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo
* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`.
+* Note that if the parameters are too lengthy, they may be truncated in the UI.
+
* 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.
-
+
+## General Commands
### Viewing help : `help`
-Shows a message explaning how to access the help page.
+Shows a message explaining how to access the help page.

Format: `help`
+Alternatively, the message can also be accessed by using the keyboard shortcut `F1` or
+through the menu bar (`Help` > `Help`).
+
+### Exiting the program : `exit`
+
+Exits the program.
+
+Format: `exit`
+
+Alternatively, the user can also exit the application through the menu bar (`File` > `Exit`)
+
+## AddressBook Management
### Adding a person: `add`
-Adds a person to the address book.
+Adds a person to the AddressBook.
Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…`
-
:bulb: **Tip:**
-A person can have any number of tags (including 0)
-
+
+
+**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`
+> **Note:** Currently UniMate does not support the use of forward-slash in the name of contacts (e.g. s/o). The developers
+ understand this, and we hope to add support for accepting forward-slashes in the contact's name in a future release.
-Shows a list of all persons in the address book.
+
-Format: `list`
+In the example, after executing
+`add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01`, we see `John Doe`'s
+contact information stored in the AddressBook as the 7th contact on the left-hand side of the GUI.
+
+### Deleting a person: `delete`
+
+Deletes the specified person from the AddressBook.
+
+Format: `delete INDEX`
+
+* Deletes the person at the specified `INDEX`
+* The index refers to the index number shown in the displayed person list
+* The index must be a **positive integer** (i.e. 1, 2, 3, …)
+
+Examples:
+* `list` followed by `delete 2` deletes the 2nd person in the AddressBook
+* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command
-### Editing a person : `edit`
+
-Edits an existing person in the address book.
+In this example, after executing `list`, followed by `delete 7`, `John Doe`, the person with index 7
+is removed from the displayed AddressBook.
+
+### Editing a person: `edit`
+
+Edits an existing person in the AddressBook.
Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…`
* 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.
+* 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.
+ 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.
+
+
+In this example, after executing `edit 1 p/12345678`, `Alex Yeoh` the person with index 1 has his
+phone number edited to `12345678`.
+
### Locating persons by name: `find`
-Finds persons whose names contain any of the given keywords.
+Finds persons whose names contain **any** of the given keywords.
+
+> **Tip:** Multiple arguments passed into the `find` command will be delimited by whitespaces, which means
+the arguments `John Doe` will be parsed into 2 separate arguments `John` and `Doe`. To circumvent
+this delimitation, consider the [filter command](#filtering-persons-by-attribute-filter).
Format: `find KEYWORD [MORE_KEYWORDS]`
-* The search is case-insensitive. e.g `hans` will 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.
+* Only the name is searched
* Only full words will be matched e.g. `Han` will not match `Hans`
-* Persons matching at least one keyword will be returned (i.e. `OR` search).
+* Persons matching at least one keyword will be returned (i.e. `OR` search)
e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang`
Examples:
@@ -127,43 +264,411 @@ Examples:
* `find alex david` returns `Alex Yeoh`, `David Li`

-### Deleting a person : `delete`
+### Filtering persons by attribute: `filter`
-Deletes the specified person from the address book.
+Filters out persons whose fields contain any of the given keywords. The keywords contained in each field will be treated as a single argument.
-Format: `delete INDEX`
+Format: `filter [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…`
+
+* Filter is case-insensitive. e.g. `cs2103` will match `CS2103`
+* The order of the fields does not matter
+* All provided fields are searched
+* All tags containing the words or part-thereof will be matched (e.g. `Ba` will return `Badminton` or `Basketball` or `Football` or `Backgammon`)
+* Only persons matching all specified fields will be returned (i.e. logical `AND` search)
+* Arguments in a specific field are not delimited (i.e. the argument `John Doe` in `filter n/John Doe` will be treated as a single argument)
+* Indicating a field but leaving it blank e.g. `filter n/` will show all contacts.
+
+Examples:
+* `filter t/CS2103` - Displays all contacts with the CS2103 tag or tags containing ``CS2103`` e.g. CS2103T
+* `filter t/MA2116 t/CS1010S n/John e/@u.nus.edu` - Displays all contacts with the ``MA2116`` and ``CS1010S`` tags as well as
+with a name containing ``John`` and an email address with the domain ``u.nus.edu``
+
+
+
+In this example, after executing `filter t/colleagues`, the AddressBook list displays all contacts
+that have the `colleagues` tag.
+
+### Listing all persons : `list`
+
+Shows a list of all persons in the AddressBook. This command is particularly useful to
+display all the contacts after using the [find](#locating-persons-by-name-find) and [filter](#filtering-persons-by-attribute-filter) commands.
+
+Format: `list`
+
+
+
+In this example, after executing the `list` command, we see the entire AddressBook again.
-* 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, …
+### Sort persons: `sort`
+
+Format: `sort /COMPARATOR [/reverse]`
+
+* Sorts all persons by specified `COMPARATOR`
+* Currently supported `COMPARATOR` include:
+ - `byname`
+ - `byemail`
+ - `byphone`
+ - `byaddress`
+* The comparator refers to the attribute(s) provided for the basis to sort
+* Only sorting by one attribute is allowed, due to the nature of most fields being unique (with the unlikely cases of name and address)
+* The sorting is done according to [ASCII](https://www.ibm.com/docs/en/cobol-zos/6.1?topic=sequences-us-english-ascii-code-page) sequence, but is case-insensitive (i.e. `adam` has precedence over `Beatrice` when sorted in ascending order)
+* Sorting is done in ascending order by default. To sort by descending order, the field `/reverse` can be added to the command (see Examples below)
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.
+* `sort /byname` sorts all contacts in UniMate AddressBook by their full name
+* `sort /byaddress /reverse` sorts all contacts in UniMate AddressBook by their address
+
+
+
+In this example, after executing `sort /byphone`, the AddressBook is now sorted according to the
+contact's phone numbers, in ascending numerical order.
### Clearing all entries : `clear`
-Clears all entries from the address book.
+Clears all entries from the AddressBook.
Format: `clear`
-### Exiting the program : `exit`
+
-Exits the program.
+In this example, after executing `clear`, the AddressBook is now cleared of all contacts.
-Format: `exit`
+## Calendar System
+
+### General Definitions
+
+> **Date Time Format**
+>* When inputting a date and time into a command, the following format is used:
+>
+> `yyyy-MM-dd HH:mm` where
+> * `yyyy` represents the year,
+> * `MM` represents the month,
+> * `dd` represents the day,
+> * `HH` represents the hours and
+> * `mm` represents the minutes.
+>
+>**Event span**
+>* The chronological span of the event is **inclusive** of the start date and time, but **exclusive** of the end date and time.
+ This means that for 2 consecutive events, the later event can start at the same time the earlier event ends.
+
+### \[USER CALENDAR\] Adding an event: `addEvent`
+
+Adds an event to the user's calendar.
+
+Format: `addEvent d/DESCRIPTION ts/START_DATE_TIME te/END_DATE_TIME`
+
+* Adds the event starting from `START_DATE_TIME` and ending at `END_DATE_TIME`
+* `DESCRIPTION` must be non-empty
+* `START_DATE_TIME` and `END_DATE_TIME` must be in `yyyy-MM-dd HH:mm` format
+* `START_DATE_TIME` must be before `END_DATE_TIME` for the command to be valid
+
+Example:
+* `addEvent d/Cry about deadlines ts/2023-01-01 13:00 te/2023-01-01 15:00`
+
+
+
+In this example, after executing the command `addEvent d/myEvent ts/2023-11-08 14:00 te/2023-11-08 16:00`,
+the event `myEvent` is now visible in both the calendar on the right and the event list at the bottom, as
+the date of `myEvent` coincides with the current week when the screenshot was taken.
+
+In the case `myEvent` happens before/after the current week, it will not be displayed in the calendar, but will still
+show in the event list.
+
+### \[USER CALENDAR\] Deleting an event: `deleteEvent`
+
+Deletes an event from the user's calendar.
+
+Format `deleteEvent DATE_TIME_DURING_EVENT`
+
+* Deletes an event at the specified date and time.
+* An event is considered to be at that date and time if the date time lies between the start time (inclusive) and the
+end time (exclusive).
+* If there is no event during `DATE_TIME_DURING_EVENT`, an error will be thrown.
+
+Example:
+`deleteEvent 2023-11-01 12:00`
+
+
+
+In this example, after executing the command `deleteEvent 2023-11-08 15:00`, `myEvent`(added in
+the [addEvent command example](#user-calendar-adding-an-event-addevent)) that is happening during the
+specified time was deleted, leaving an empty calendar and event list.
+
+### \[USER CALENDAR\] Deleting multiple events: `clearEvents`
+
+Clears all events within a specified time range.
+
+> **Note:** In order to ensure the user does not make the mistake of deleting more events than intended,
+this command requires additional **confirmation** (refer to command format below) from the user to fully execute.
+Without the confirmation, the result box will instead display the list of events that will be deleted if the
+command were to fully execute, for the user's verification.
+
+Format: `clearEvents ts/START_DATE_TIME te/END_DATE_TIME c/CONFIRMATION`
+
+* Deletes all events from the specified start date and time to the specified end date and time
+* An event is considered to be within the time range if overlaps with the time range for any period of time, inclusive of `START_DATE_TIME` but exclusive of `END_DATE_TIME`
+* When the `CONFIRMATION` is absent, the command shows all events within the time range but does not delete them. The
+same command is then shown with the confirmation included that can be copied and pasted to execute the command
+* The `START_DATE_TIME` must be before `END_DATE_TIME`
+* If there is no `START_DATE_TIME` or `END_DATE_TIME`, an error will be thrown
+
+Example:
+`clearEvents ts/2023-01-01 00:00 te/2023-12-31 23:59 c/CONFIRMED`
+
+
+
+In this example, after executing `clearEvents ts/2023-11-08 14:00 te/2023-11-08 16:00 c/CONFIRMED`, `myEvent`(added in
+the [addEvent command example](#user-calendar-adding-an-event-addevent)) that is happening during the duration of the
+specified time was deleted, leaving an empty calendar and event list.
+
+### Event List/Task List switch: `switchList`
+
+Events can be viewed from the calendar GUI that appears on the right.
+
+Additionally, a list of all events are displayed by default at the bottom. This list at the bottom can be switched to a
+task list with the `switchList` command. More information can be found under at [Viewing all Tasks](#task-management-system).
+
+### \[CONTACT CALENDAR\] Adding an event to a contact: `addContactEvent`
+
+Adds an event to a contact's calendar at the specified index.
+
+Format: `addContactEvent INDEX d/DESCRIPTION ts/START_DATE_TIME ts/END_DATE_TIME`
+
+* Adds the event starting from `START_DATE_TIME` and ending at `END_DATE_TIME`
+* `START_DATE_TIME` and `END_DATE_TIME` must be in `yyyy-MM-dd HH:mm` format
+* `START_DATE_TIME` must be before `END_DATE_TIME` for the command to be valid
+
+Example:
+* `addContactEvent 1 d/Team Meeting ts/2024-01-01 09:00 te/2024-01-01 11:00`
+
+
+
+In this example, after executing `addContactEvent 1 d/Alex's Event ts/2023-11-08 14:00 te/2023-11-08 18:00` and
+[double-clicking on the person card](#main-feature-1-addressbook) of index 1 in the AddressBook, the
+contact's calendar pops up, revealing `Alex's Event`. In the case that the event happens before/after
+the current week, it will not be shown in the calendar, but will still be displayed in the
+[contact's event list](#viewing-contact-event-list-viewcontactevents).
+
+### \[CONTACT CALENDAR\] Deleting an event from a contact: `deleteContactEvent`
+
+Deletes an event from a contact's calendar at the specified index.
+
+Format `deleteContactEvent INDEX ts/DATE_TIME`
+
+* Deletes an event that contains the `DATE_TIME` from the contact
+
+Example:
+* `deleteContactEvent 1 ts/2024-01-01 09:00`
+
+
+
+In this example, after executing `deleteContactEvent 1 ts/2023-11-08 14:00` and
+[double-clicking on the person card](#main-feature-1-addressbook) of index 1 in the AddressBook,
+the contact's calendar pops up, revealing an empty calendar as `Alex's Event` (added in the
+[example in addContactEvent](#contact-calendar-adding-an-event-to-a-contact-addcontactevent)),
+occurring at the specified time, has been removed.
+
+### \[CONTACT CALENDAR\] Edit Contact Calendar Event: `editContactEvent`
+
+Edits the details of an event in a contact's calendar.
+
+> **Note:** If this command is executed while the event list of the contact of interest is open,
+the event list will not be updated until the tab is closed and open again.
+
+Format: `editContactEvent PERSON_INDEX EVENT_INDEX [d/DESCRIPTION] [ts/NEW_START_DATE_TIME][te/NEW_END_DATE_TIME]`
+
+* Edits `EVENT_INDEX` event of the `PERSON_INDEX` person in the AddressBook with the given fields.
+
+Example: `editContactEvent 1 1 d/Nap`, `editContactEvent 2 3 ts/2023-10-10 10:00 te/2023-10-12 15:00`
+
+* Note that all edited fields are optional, but there must be at least 1 edited field.
+
+
+
+In this example, we see this is the state of the calendar of `Alex Yeoh`, the person with index 1
+in the AddressBook.
+
+
+
+After executing `editContactEvent 1 1 d/Edited Description`, we get this confirmation message
+
+
+
+We can see that the event `Nap` has its description changed to `Edited Description`.
+
+### Viewing contact event list: `viewContactEvents`
+
+Creates a pop-up that displays a list of all events of a calendar belonging to a person in the AddressBook.
+
+Format: `viewContactEvents INDEX`
+
+* Views the event list of the person at `INDEX` as displayed.
+
+Example:
+* `viewContactEvents 1`
+
+
+
+In this example, after execute `viewContactEvents 1`, a pop-up with the list of events of the person
+with index 1 in the AddressBook, which in this case is `Alex Yeoh` shows up.
+
+### Comparing calendars with AddressBook Contacts
+There are 2 ways for the user to compare calendars with their AddressBook contacts:
+- [Index](#1-comparison-by-index)
+- [Tag](#2-comparison-by-tag)
+
+The resulting pop-up calendar will display the time periods where all parties
+are not available as greyed out sections. The pop-up has to be closed in order for the user to access
+the main application again.
+
+>**Note:** Arguments for the commands are optional, hence `compareCalendars` and
+`compareGroupCalendars` are valid commands, but the resulting pop-up will just display the
+user's calendar.
+
+#### 1. Comparison by index
+
+Format `compareCalendars [INDEX]...`
+
+* Compare calendar with the contacts at the respective `INDEX`
+* `INDEX` must be a positive non-zero integer that is smaller or equals to the size of the AddressBook
+* If the `INDEX` number provided is invalid, an error will be returned
+* If no `INDEX` is supplied, the resulting pop-up will just display the user's calendar
+
+Example:
+`compareCalendars 1 3 5`
+
+
+
+In this example, after executing `compareCalendars 1`, we see the timings when both the user and
+the person with index 1 (`Alex Yeoh`) are not free is blocked out.
+
+#### 2. Comparison by tag
+
+Format `compareGroupCalendars [TAG]...`
+
+* Compare calendar with the contacts with the specified `TAG`
+* If some of the `TAG` provided are invalid, the resulting pop-up will ignore the invalid `TAG`
+* If all the `TAG` provided are invalid, the resulting pop-up will just display the user's calendar
+* If no `TAG` is supplied, the resulting pop-up will just display the user's calendar
+
+Example:
+`compareGroupCalendars school friends`
+
+
+
+In this example, after executing `compareGroupCalendars friends`, we see the timings when both the
+user and the contacts that have the `friends` tag are unavailable are blocked out.
+
+### Import *.ics files (Coming Soon)
+
+User can import *.ics files, which will automatically be integrated into
+their UniMate calendars.
+
+Format: `import FILE_PATH`
+
+### View other weeks of Calendar (Coming Soon)
+
+User can view other weeks of their calendar schedule beyond just the
+current week.
+
+Format: `viewWeek DATE`
+
+## Task Management System
+
+Tasks consist of a `DESCRIPTION` and an **optional** `DEADLINE`.
+
+> **Note:** Tasks can have the same `DESCRIPTION` or `DEADLINE`, but not both.
+
+### Viewing tasks: `switchList`
+
+Switches the bottom list between the event list and the task list.
+
+Format: `switchList`
+
+* All input after `switchList` will be ignored.
+* The bottom list displays the event list by default.
+
+
+
+In this example, after executing `switchList`, we see that the bottom of the GUI has switched from
+[`event list`](#sub-feature-2-event-list) to the [`task list`](#main-feature-3-task-management-system).
+
+### Adding tasks: `addTask`
+
+Adds a task to the Task Manager.
+
+Format: `addTask d/DESCRIPTION [te/DEADLINE]`
+
+* `DESCRIPTION` cannot be empty.
+* `DEADLINE` must be in the same format given [above](#general-definitions) for date and time.
+* `DEADLINE` can also be omitted to create a task with no specified deadline.
+* When adding a task with a deadline with a date that is invalid but with a day of 31 or less, instead adds a task with
+a deadline on the last day of the month.
+* While there is no character limit for Task descriptions, descriptions will be truncated if they are too long to
+display.
+
+Examples:
+* `addTask d/Go for a run te/2023-02-14 19:00`
+* `addTask d/Hydrate regularly!`
+* `addTask d/Fix CS2103 Project bug te/2023-02-31 12:00` creates the task with deadline `2023-02-28 12:00` instead.
+
+
+
+In this example, after executing `addTask d/Hydrate regularly!`, we see the new `Hydrate regularly!`
+task appearing at the bottom of the task list.
+
+### Deleting tasks: `deleteTask`
+
+Deletes a task from the Task Manager according to the index of the task displayed in the task list.
+
+Format: `deleteTask INDEX`
+
+* Throws an error if there is no `INDEX` present or if it exceeds the length of the task list.
+
+Examples:
+* `deleteTask 1`
+
+
+
+In this example, after executing `deleteTask 4`, the `Hydrate regularly!` task added in the
+[previous example](#adding-tasks-addtask) was removed.
+
+### Sorting tasks: `sortTasks`
+
+Changes the way tasks in the Task Manager are displayed in the task list.
+
+Format: `sortTasks PARAMETER`
+
+* `PARAMETER` can only be `DESCRIPTION` or `DEADLINE`.
+
+Examples:
+* `sortTasks DESCRIPTION` sorts tasks by their `DESCRIPTION` alphabetically.
+* `sortTasks DEADLINE` sorts tasks by their `DEADLINE`. Tasks with deadlines are prioritised above tasks with no
+deadline.
+
+
+
+In this example, after executing `sortTasks DESCRIPTION`, the tasks are now sorted in ascending
+alphanumeric order.
+
+## Miscellaneous
### 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.
+All data is 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.
+AddressBook data is saved automatically as a JSON file `[JAR file location]/data/addressbook.json`.
+Calendar data is saved automatically as a JSON file `[JAR file location]/data/calendar.json`.
+Task-list data is saved automatically as a JSON file `[JAR file location]/data/taskmanager.json`.
+Advanced users are welcome to update data directly by editing the data files.
+
+
-
: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.
-
+**Caution:**
+If your changes to the data file makes its format invalid, UniMate 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]`
@@ -171,27 +676,44 @@ _Details coming soon ..._
--------------------------------------------------------------------------------------------------------------------
-## 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**: 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 UniMate home folder.
--------------------------------------------------------------------------------------------------------------------
-## 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.
+2. If the zoom setting of the operating system is not set to 100%, it might cause inconsistencies in the UI which may cause the event cards displayed in the [**Calendar GUI**](#features-overview) to be lined up incorrectly.
--------------------------------------------------------------------------------------------------------------------
-## Command summary
+# 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`
+Action | Format, Examples
+-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------
**Help** | `help`
+**Exit** | `exit`
+**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`
+**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`
+**Filter** | `filter [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…` e.g. `filter n/John Doe`
+**List** | `list`
+**Sort** | `sort /COMPARATOR` e.g. `sort /byname`
+**Clear** | `clear`
+**addEvent** | `addEvent d/DESCRIPTION ts/START_DATE_TIME te/END_DATE_TIME` e.g. `addEvent d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+**deleteEvent** | `deleteEvent DATE_TIME` e.g. `deleteEvent 2023-02-03 12:00`
+**clearEvents** | `clearEvents ts/START_DATE_TIME te/END_DATE_TIME c/CONFIRMATION` e.g. `clearEvents ts/2023-02-03 12:00 te/2023-02-03 14:00 c/CONFIRMED`
+**switchList** | `switchList`
+**addContactEvent** | `addContactEvent INDEX d/DESCRIPTION ts/START_DATE_TIME te/END_DATE_TIME` e.g. `addContactEvent 1 d/Cry about deadlines ts/2023-01-01 00:01 te/2023-12-31 23:59`
+**deleteContactEvent** | `deleteContactEvent INDEX ts/DATE_TIME` e.g. `deleteContactEvent 1 ts/2023-02-03 12:00`
+**editContactEvent** | `editContactEvent PERSON_INDEX EVENT_INDEX [d/DESCRIPTION] [ts/NEW_START_DATE_TIME] [te/NEW_END_DATE_TIME]` e.g. `editContactEvent 1 1 d/Edited Description`
+**viewContactEvents** | `viewContactEvents INDEX` e.g. `viewContactEvents 1`
+**compareCalendars** | `compareCalendars [INDEX]...` e.g. `compareCalendars 1 3 5`
+**compareGroupCalendars** | `compareGroupCalendars [TAG]...` e.g. `compareGroupCalendars school friend`
+**addTask** | `addTask d/DESCRIPTION [te/DEADLINE]` e.g. `addTask d/Go for a run te/2023-02-14 19:00`
+**deleteTask** | `deleteTask INDEX` e.g. `deleteTask 1`
+**sortTasks** | `sortTasks PARAMETER` e.g. `sortTasks DESCRIPTION` e.g. `sortTasks DEADLINE`
diff --git a/docs/_config.yml b/docs/_config.yml
deleted file mode 100644
index 6bd245d8f4e..00000000000
--- a/docs/_config.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-title: "AB-3"
-theme: minima
-
-header_pages:
- - UserGuide.md
- - DeveloperGuide.md
- - AboutUs.md
-
-markdown: kramdown
-
-repository: "se-edu/addressbook-level3"
-github_icon: "images/github-icon.png"
-
-plugins:
- - jemoji
diff --git a/docs/_data/projects.yml b/docs/_data/projects.yml
deleted file mode 100644
index 8f3e50cb601..00000000000
--- a/docs/_data/projects.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-- name: "AB-1"
- url: https://se-edu.github.io/addressbook-level1
-
-- name: "AB-2"
- url: https://se-edu.github.io/addressbook-level2
-
-- name: "AB-3"
- url: https://se-edu.github.io/addressbook-level3
-
-- name: "AB-4"
- url: https://se-edu.github.io/addressbook-level4
-
-- name: "Duke"
- url: https://se-edu.github.io/duke
-
-- name: "Collate"
- url: https://se-edu.github.io/collate
-
-- name: "Book"
- url: https://se-edu.github.io/se-book
-
-- name: "Resources"
- url: https://se-edu.github.io/resources
diff --git a/docs/_includes/custom-head.html b/docs/_includes/custom-head.html
deleted file mode 100644
index 8559a67ffad..00000000000
--- a/docs/_includes/custom-head.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% comment %}
- Placeholder to allow defining custom head, in principle, you can add anything here, e.g. favicons:
-
- 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/head.html b/docs/_includes/head.html
deleted file mode 100644
index 83ac5326933..00000000000
--- a/docs/_includes/head.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
- {%- include custom-head.html -%}
-
- {{page.title}}
-
-
diff --git a/docs/_includes/header.html b/docs/_includes/header.html
deleted file mode 100644
index 33badcd4f99..00000000000
--- a/docs/_includes/header.html
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
+
-:information_source: Don’t forget to update `AddressBookParser` to use our new `RemarkCommandParser`!
+Don’t forget to update `AddressBookParser` to use our new `RemarkCommandParser`!
-
+
If you are stuck, check out the sample
[here](https://github.com/se-edu/addressbook-level3/commit/dc6d5139d08f6403da0ec624ea32bd79a2ae0cbf#diff-8bf239e8e9529369b577701303ddd96af93178b4ed6735f91c2d8488b20c6b4a).
@@ -244,7 +247,7 @@ Simply add the following to [`seedu.address.ui.PersonCard`](https://github.com/s
**`PersonCard.java`:**
-``` java
+```java
@FXML
private Label remark;
```
@@ -276,11 +279,11 @@ We change the constructor of `Person` to take a `Remark`. We will also need to d
Unfortunately, a change to `Person` will cause other commands to break, you will have to modify these commands to use the updated `Person`!
-
+
-:bulb: Use the `Find Usages` feature in IntelliJ IDEA on the `Person` class to find these commands.
+Use the `Find Usages` feature in IntelliJ IDEA on the `Person` class to find these commands.
-
+
Refer to [this commit](https://github.com/se-edu/addressbook-level3/commit/ce998c37e65b92d35c91d28c7822cd139c2c0a5c) and check that you have got everything in order!
@@ -291,11 +294,11 @@ AddressBook stores data by serializing `JsonAdaptedPerson` into `json` with the
While the changes to code may be minimal, the test data will have to be updated as well.
-
+
-:exclamation: You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book!
+You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book!
-
+
Check out [this commit](https://github.com/se-edu/addressbook-level3/commit/556cbd0e03ff224d7a68afba171ad2eb0ce56bbf)
to see what the changes entail.
@@ -308,7 +311,7 @@ Just add [this one line of code!](https://github.com/se-edu/addressbook-level3/c
**`PersonCard.java`:**
-``` java
+```java
public PersonCard(Person person, int displayedIndex) {
//...
remark.setText(person.getRemark().value);
@@ -328,7 +331,7 @@ save it with `Model#setPerson()`.
**`RemarkCommand.java`:**
-``` java
+```java
//...
public static final String MESSAGE_ADD_REMARK_SUCCESS = "Added remark to Person: %1$s";
public static final String MESSAGE_DELETE_REMARK_SUCCESS = "Removed remark from Person: %1$s";
diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md
index f29169bc924..c73bd379e5e 100644
--- a/docs/tutorials/RemovingFields.md
+++ b/docs/tutorials/RemovingFields.md
@@ -1,8 +1,11 @@
---
-layout: page
-title: "Tutorial: Removing Fields"
+ layout: default.md
+ title: "Tutorial: Removing Fields"
+ pageNav: 3
---
+# Tutorial: Removing Fields
+
> Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.
>
> — Antoine de Saint-Exupery
@@ -10,17 +13,17 @@ title: "Tutorial: Removing Fields"
When working on an existing code base, you will most likely find that some features that are no longer necessary.
This tutorial aims to give you some practice on such a code 'removal' activity by removing the `address` field from `Person` class.
-
+
**If you have done the [Add `remark` command tutorial](AddRemark.html) already**, you should know where the code had to be updated to add the field `remark`. From that experience, you can deduce where the code needs to be changed to _remove_ that field too. The removing of the `address` field can be done similarly.
However, if you have no such prior knowledge, removing a field can take a quite a bit of detective work. This tutorial takes you through that process. **At least have a read even if you don't actually do the steps yourself.**
-
+
-* Table of Contents
-{:toc}
+
+
## Safely deleting `Address`
@@ -50,10 +53,10 @@ Let’s try removing references to `Address` in `EditPersonDescriptor`.
1. Remove the usages of `address` and select `Do refactor` when you are done.
-
+
- :bulb: **Tip:** Removing usages may result in errors. Exercise discretion and fix them. For example, removing the `address` field from the `Person` class will require you to modify its constructor.
-
+ **Tip:** Removing usages may result in errors. Exercise discretion and fix them. For example, removing the `address` field from the `Person` class will require you to modify its constructor.
+
1. Repeat the steps for the remaining usages of `Address`
@@ -71,7 +74,7 @@ A quick look at the `PersonCard` class and its `fxml` file quickly reveals why i
**`PersonCard.java`**
-``` java
+```java
...
@FXML
private Label address;
diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md
index 4fb62a83ef6..2b1b0f2d6b7 100644
--- a/docs/tutorials/TracingCode.md
+++ b/docs/tutorials/TracingCode.md
@@ -1,26 +1,30 @@
---
-layout: page
-title: "Tutorial: Tracing code"
+ layout: default.md
+ title: "Tutorial: Tracing code"
+ pageNav: 3
---
+# Tutorial: Tracing code
+
+
> Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …\[Therefore,\] making it easy to read makes it easier to write.
>
> — Robert C. Martin Clean Code: A Handbook of Agile Software Craftsmanship
When trying to understand an unfamiliar code base, one common strategy used is to trace some representative execution path through the code base. One easy way to trace an execution path is to use a debugger to step through the code. In this tutorial, you will be using the IntelliJ IDEA’s debugger to trace the execution path of a specific user command.
-* Table of Contents
-{:toc}
+
+
## Before we start
Before we jump into the code, it is useful to get an idea of the overall structure and the high-level behavior of the application. This is provided in the 'Architecture' section of the developer guide. In particular, the architecture diagram (reproduced below), tells us that the App consists of several components.
-
+
It also has a sequence diagram (reproduced below) that tells us how a command propagates through the App.
-
+
Note how the diagram shows only the execution flows _between_ the main components. That is, it does not show details of the execution path *inside* each component. By hiding those details, the diagram aims to inform the reader about the overall execution path of a command without overwhelming the reader with too much details. In this tutorial, you aim to find those omitted details so that you get a more in-depth understanding of how the code works.
@@ -37,16 +41,16 @@ As you know, the first step of debugging is to put in a breakpoint where you wan
In our case, we would want to begin the tracing at the very point where the App start processing user input (i.e., somewhere in the UI component), and then trace through how the execution proceeds through the UI component. However, the execution path through a GUI is often somewhat obscure due to various *event-driven mechanisms* used by GUI frameworks, which happens to be the case here too. Therefore, let us put the breakpoint where the `UI` transfers control to the `Logic` component.
-
+
According to the sequence diagram you saw earlier (and repeated above for reference), the `UI` component yields control to the `Logic` component through a method named `execute`. Searching through the code base for an `execute()` method that belongs to the `Logic` component yields a promising candidate in `seedu.address.logic.Logic`.
-
+
-:bulb: **Intellij Tip:** The ['**Search Everywhere**' feature](https://www.jetbrains.com/help/idea/searching-everywhere.html) can be used here. In particular, the '**Find Symbol**' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a _method_ named `execute`, not simply the text `execute`.
-
+**Intellij Tip:** The ['**Search Everywhere**' feature](https://www.jetbrains.com/help/idea/searching-everywhere.html) can be used here. In particular, the '**Find Symbol**' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a _method_ named `execute`, not simply the text `execute`.
+
A quick look at the `seedu.address.logic.Logic` (an extract given below) confirms that this indeed might be what we’re looking for.
@@ -67,14 +71,14 @@ public interface Logic {
But apparently, this is an interface, not a concrete implementation.
That should be fine because the [Architecture section of the Developer Guide](../DeveloperGuide.html#architecture) tells us that components interact through interfaces. Here's the relevant diagram:
-
+
Next, let's find out which statement(s) in the `UI` code is calling this method, thus transferring control from the `UI` to the `Logic`.
-
+
-:bulb: **Intellij Tip:** The ['**Find Usages**' feature](https://www.jetbrains.com/help/idea/find-highlight-usages.html#find-usages) can find from which parts of the code a class/method/variable is being used.
-
+**Intellij Tip:** The ['**Find Usages**' feature](https://www.jetbrains.com/help/idea/find-highlight-usages.html#find-usages) can find from which parts of the code a class/method/variable is being used.
+

@@ -87,10 +91,10 @@ Now let’s set the breakpoint. First, double-click the item to reach the corres
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`.
-
+
-:bulb: **Tip:** Over the course of the debugging session, you will encounter every major component in the application. Try to keep track of what happens inside the component and where the execution transfers to another component.
-
+**Tip:** Over the course of the debugging session, you will encounter every major component in the application. Try to keep track of what happens inside the component and where the execution transfers to another component.
+
1. To start the debugging session, simply `Run` \> `Debug Main`
@@ -110,7 +114,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
**LogicManager\#execute().**
- ``` java
+ ```java
@Override
public CommandResult execute(String commandText)
throws CommandException, ParseException {
@@ -142,7 +146,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [

1. _Step into_ the line where user input in parsed from a String to a Command, which should bring you to the `AddressBookParser#parseCommand()` method (partial code given below):
- ``` java
+ ```java
public Command parseCommand(String userInput) throws ParseException {
...
final String commandWord = matcher.group("commandWord");
@@ -157,7 +161,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
1. Stepping through the `switch` block, we end up at a call to `EditCommandParser().parse()` as expected (because the command we typed is an edit command).
- ``` java
+ ```java
...
case EditCommand.COMMAND_WORD:
return new EditCommandParser().parse(arguments);
@@ -166,8 +170,10 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
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()`.
-
: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!
-
+
+
+ **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!
+
1. Stepping through the method shows that it calls `ArgumentTokenizer#tokenize()` and `ParserUtil#parseIndex()` to obtain the arguments and index required.
@@ -175,17 +181,17 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [

1. As you just traced through some code involved in parsing a command, you can take a look at this class diagram to see where the various parsing-related classes you encountered fit into the design of the `Logic` component.
-
+
1. Let’s continue stepping through until we return to `LogicManager#execute()`.
The sequence diagram below shows the details of the execution path through the Logic component. Does the execution path you traced in the code so far match the diagram?
- 
+
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()`:**
- ``` java
+ ```java
@Override
public CommandResult execute(Model model) throws CommandException {
...
@@ -205,25 +211,28 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
* 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.
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.
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:
-
+
+
* :bulb: This may be a good time to read through the [`Logic` component section of the DG](../DeveloperGuide.html#logic-component)
1. Similar to before, you can step over/into statements in the `LogicManager#execute()` method to examine how the control is transferred to the `Storage` component and what happens inside that component.
-
:bulb: **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into.
-
+
+
+ **Intellij Tip:** When trying to step into a statement such as `storage.saveAddressBook(model.getAddressBook())` which contains multiple method calls, Intellij will let you choose (by clicking) which one you want to step into.
+
-1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability):
+1. As you step through the code inside the `Storage` component, you will eventually arrive at the `JsonAddressBook#saveAddressBook()` method which calls the `JsonSerializableAddressBook` constructor, to create an object that can be _serialized_ (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability):
**`JsonSerializableAddressBook` constructor:**
- ``` java
+ ```java
/**
* Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use.
*
@@ -243,7 +252,8 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
This is because regular Java objects need to go through an _adaptation_ for them to be suitable to be saved in JSON format.
1. While you are stepping through the classes in the `Storage` component, here is the component's class diagram to help you understand how those classes fit into the structure of the component.
-
+
+
* :bulb: This may be a good time to read through the [`Storage` component section of the DG](../DeveloperGuide.html#storage-component)
1. We can continue to step through until you reach the end of the `LogicManager#execute()` method and return to the `MainWindow#executeCommand()` method (the place where we put the original breakpoint).
@@ -251,7 +261,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [
1. Stepping into `resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser());`, we end up in:
**`ResultDisplay#setFeedbackToUser()`**
- ``` java
+ ```java
public void setFeedbackToUser(String feedbackToUser) {
requireNonNull(feedbackToUser);
resultDisplay.setText(feedbackToUser);
diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/seedu/address/Main.java
index ec1b7958746..51e41d70f4d 100644
--- a/src/main/java/seedu/address/Main.java
+++ b/src/main/java/seedu/address/Main.java
@@ -25,7 +25,6 @@ public class Main {
private static Logger logger = LogsCenter.getLogger(Main.class);
public static void main(String[] args) {
-
// As per https://github.com/openjdk/jfx/blob/master/doc-files/release-notes-16.md
// JavaFX 16 (or later) runtime logs a warning at startup if JavaFX classes are loaded from
// the classpath instead of a module.
diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java
index 3d6bd06d5af..8778affaeea 100644
--- a/src/main/java/seedu/address/MainApp.java
+++ b/src/main/java/seedu/address/MainApp.java
@@ -21,12 +21,22 @@
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.model.ReadOnlyUserPrefs;
import seedu.address.model.UserPrefs;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.calendar.UniMateCalendar;
+import seedu.address.model.task.ReadOnlyTaskManager;
+import seedu.address.model.task.TaskManager;
+import seedu.address.model.util.SampleCalendarUtil;
import seedu.address.model.util.SampleDataUtil;
+import seedu.address.model.util.SampleTaskManagerUtil;
import seedu.address.storage.AddressBookStorage;
+import seedu.address.storage.CalendarStorage;
import seedu.address.storage.JsonAddressBookStorage;
+import seedu.address.storage.JsonCalendarStorage;
+import seedu.address.storage.JsonTaskManagerStorage;
import seedu.address.storage.JsonUserPrefsStorage;
import seedu.address.storage.Storage;
import seedu.address.storage.StorageManager;
+import seedu.address.storage.TaskManagerStorage;
import seedu.address.storage.UserPrefsStorage;
import seedu.address.ui.Ui;
import seedu.address.ui.UiManager;
@@ -36,7 +46,7 @@
*/
public class MainApp extends Application {
- public static final Version VERSION = new Version(0, 2, 2, true);
+ public static final Version VERSION = new Version(1, 4, 0, true);
private static final Logger logger = LogsCenter.getLogger(MainApp.class);
@@ -58,7 +68,9 @@ public void init() throws Exception {
UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath());
UserPrefs userPrefs = initPrefs(userPrefsStorage);
AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath());
- storage = new StorageManager(addressBookStorage, userPrefsStorage);
+ CalendarStorage calendarStorage = new JsonCalendarStorage(userPrefs.getCalendarFilePath());
+ TaskManagerStorage taskManagerStorage = new JsonTaskManagerStorage(userPrefs.getTaskManagerFilePath());
+ storage = new StorageManager(addressBookStorage, calendarStorage, taskManagerStorage, userPrefsStorage);
model = initModelManager(storage, userPrefs);
@@ -76,21 +88,52 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) {
logger.info("Using data file : " + storage.getAddressBookFilePath());
Optional addressBookOptional;
- ReadOnlyAddressBook initialData;
+ ReadOnlyAddressBook addressBookInitialData;
+ Optional calendarOptional;
+ ReadOnlyCalendar calendarInitialData;
+ Optional taskManagerOptional;
+ ReadOnlyTaskManager taskManagerInitialData;
+
try {
addressBookOptional = storage.readAddressBook();
if (!addressBookOptional.isPresent()) {
logger.info("Creating a new data file " + storage.getAddressBookFilePath()
+ " populated with a sample AddressBook.");
}
- initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook);
+ addressBookInitialData = 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.");
- initialData = new AddressBook();
+ addressBookInitialData = new AddressBook();
+ }
+
+ try {
+ calendarOptional = storage.readCalendar();
+ if (!calendarOptional.isPresent()) {
+ logger.info("Creating a new data file " + storage.getCalendarFilePath()
+ + " populated with a sample Calendar.");
+ }
+ calendarInitialData = calendarOptional.orElseGet(SampleCalendarUtil::getSampleCalendar);
+ } catch (DataLoadingException e) {
+ logger.warning("Data file at " + storage.getCalendarFilePath() + " could not be loaded."
+ + " Will be starting with an empty Calendar.");
+ calendarInitialData = new UniMateCalendar();
+ }
+
+ try {
+ taskManagerOptional = storage.readTaskManager();
+ if (!taskManagerOptional.isPresent()) {
+ logger.info("Creating a new data file " + storage.getTaskManagerFilePath()
+ + " populated with a sample Task Manager.");
+ }
+ taskManagerInitialData = taskManagerOptional.orElseGet(SampleTaskManagerUtil::getSampleTaskManager);
+ } catch (DataLoadingException e) {
+ logger.warning("Data file at " + storage.getTaskManagerFilePath() + " could not be loaded."
+ + " Will be starting with an empty Task Manager.");
+ taskManagerInitialData = new TaskManager();
}
- return new ModelManager(initialData, userPrefs);
+ return new ModelManager(addressBookInitialData, calendarInitialData, taskManagerInitialData, userPrefs);
}
private void initLogging(Config config) {
diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java
index 92cd8fa605a..7226c15f97b 100644
--- a/src/main/java/seedu/address/logic/Logic.java
+++ b/src/main/java/seedu/address/logic/Logic.java
@@ -8,7 +8,11 @@
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.ReadOnlyAddressBook;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.event.Event;
import seedu.address.model.person.Person;
+import seedu.address.model.task.ReadOnlyTaskManager;
+import seedu.address.model.task.Task;
/**
* API of the Logic component
@@ -30,6 +34,32 @@ public interface Logic {
*/
ReadOnlyAddressBook getAddressBook();
+ /**
+ * Returns the Calendar.
+ *
+ * @see seedu.address.model.Model#getCalendar()
+ */
+ ReadOnlyCalendar getCalendar();
+
+ /**
+ * Returns the TaskManager.
+ *
+ * @see seedu.address.model.Model#getTaskManager()
+ */
+ ReadOnlyTaskManager getTaskManager();
+
+ /** Returns an unmodifiable view of the filtered list of events */
+ ObservableList getEventList();
+
+ /** Returns an unmodifiable view of the list of tasks */
+ ObservableList getTaskList();
+
+ /** Returns an unmodifiable view of the filtered list of events for the week */
+ ObservableList getCurrentWeekEventList();
+
+ /** Returns the resulting calendar from comparing calendars */
+ ReadOnlyCalendar getComparisonCalendar();
+
/** Returns an unmodifiable view of the filtered list of persons */
ObservableList getFilteredPersonList();
diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java
index 5aa3b91c7d0..c217f06370b 100644
--- a/src/main/java/seedu/address/logic/LogicManager.java
+++ b/src/main/java/seedu/address/logic/LogicManager.java
@@ -11,11 +11,15 @@
import seedu.address.logic.commands.Command;
import seedu.address.logic.commands.CommandResult;
import seedu.address.logic.commands.exceptions.CommandException;
-import seedu.address.logic.parser.AddressBookParser;
+import seedu.address.logic.parser.UniMateParser;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.Model;
import seedu.address.model.ReadOnlyAddressBook;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.event.Event;
import seedu.address.model.person.Person;
+import seedu.address.model.task.ReadOnlyTaskManager;
+import seedu.address.model.task.Task;
import seedu.address.storage.Storage;
/**
@@ -26,12 +30,15 @@ public class LogicManager implements Logic {
public static final String FILE_OPS_PERMISSION_ERROR_FORMAT =
"Could not save data to file %s due to insufficient permissions to write to the file or the folder.";
-
+ public static final String FILE_NOT_FOUND_EXCEPTION_MESSAGE = "File was moved/modified";
+ public static final String FILE_PARSE_EXCEPTION_MESSAGE = "Data in the file could not be parsed.";
+ public static final String FEATURE_NOT_IMPLEMENTED_YET = "Feature coming soon...";
+ private static final String TIMEZONE_STRING = "Asia/Singapore";
private final Logger logger = LogsCenter.getLogger(LogicManager.class);
private final Model model;
private final Storage storage;
- private final AddressBookParser addressBookParser;
+ private final UniMateParser uniMateParser;
/**
* Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}.
@@ -39,7 +46,7 @@ public class LogicManager implements Logic {
public LogicManager(Model model, Storage storage) {
this.model = model;
this.storage = storage;
- addressBookParser = new AddressBookParser();
+ uniMateParser = new UniMateParser();
}
@Override
@@ -47,11 +54,13 @@ public CommandResult execute(String commandText) throws CommandException, ParseE
logger.info("----------------[USER COMMAND][" + commandText + "]");
CommandResult commandResult;
- Command command = addressBookParser.parseCommand(commandText);
+ Command command = uniMateParser.parseCommand(commandText);
commandResult = command.execute(model);
try {
storage.saveAddressBook(model.getAddressBook());
+ storage.saveCalendar(model.getCalendar());
+ storage.saveTaskManager(model.getTaskManager());
} catch (AccessDeniedException e) {
throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e);
} catch (IOException ioe) {
@@ -66,6 +75,36 @@ public ReadOnlyAddressBook getAddressBook() {
return model.getAddressBook();
}
+ @Override
+ public ReadOnlyCalendar getCalendar() {
+ return model.getCalendar();
+ }
+
+ @Override
+ public ReadOnlyTaskManager getTaskManager() {
+ return model.getTaskManager();
+ }
+
+ @Override
+ public ObservableList getEventList() {
+ return model.getEventList();
+ }
+
+ @Override
+ public ObservableList getTaskList() {
+ return model.getTaskList();
+ }
+
+ @Override
+ public ObservableList getCurrentWeekEventList() {
+ return model.getCurrentWeekEventList();
+ }
+
+ @Override
+ public ReadOnlyCalendar getComparisonCalendar() {
+ return model.getComparisonCalendar();
+ }
+
@Override
public ObservableList getFilteredPersonList() {
return model.getFilteredPersonList();
diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java
index ecd32c31b53..3eb0ab0cab3 100644
--- a/src/main/java/seedu/address/logic/Messages.java
+++ b/src/main/java/seedu/address/logic/Messages.java
@@ -1,11 +1,15 @@
package seedu.address.logic;
+import java.time.format.DateTimeFormatter;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import javafx.collections.ObservableList;
import seedu.address.logic.parser.Prefix;
+import seedu.address.model.event.Event;
import seedu.address.model.person.Person;
+import seedu.address.model.task.Task;
/**
* Container for user visible messages.
@@ -18,6 +22,12 @@ public class Messages {
public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!";
public static final String MESSAGE_DUPLICATE_FIELDS =
"Multiple values specified for the following single-valued field(s): ";
+ public static final String MESSAGE_INTEGER_OVERFLOW =
+ "Provided index is greater than 2147483647. Please use a smaller integer.";
+ public static final String MESSAGE_INDEX_OUT_OF_BOUNDS = "Event index is out of bounds!";
+ public static final String MESSAGE_DATE_CHANGE_SUCCESSFUL = "Successfully changed!";
+ private static final String MESSAGE_EVENT_ADDED_SUCCESSFULLY = "Event added successfully!";
+ private static final String MESSAGE_EVENT_TIMING_CONFLICT = "Conflicting timing!";
/**
* Returns an error message indicating the duplicate prefixes.
@@ -48,4 +58,54 @@ public static String format(Person person) {
return builder.toString();
}
+ /**
+ * Formats the event for display to the user.
+ *
+ * @param event event to be displayed.
+ * @return String of appropriate format.
+ */
+ public static String format(Event event) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(event.getDescription())
+ .append("; ")
+ .append(event.getEventPeriod());
+ return builder.toString();
+ }
+
+ /**
+ * Formats a task for display to the user.
+ *
+ * @param task task to be displayed
+ * @return String of appropriate format.
+ */
+ public static String format(Task task) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(task.getDescriptionString())
+ .append("\nDeadline: ")
+ .append(task.getDeadlineString());
+ return builder.toString();
+ }
+
+ /**
+ * Formats the {@code person} for display to the user.
+ */
+ public static String formatCalendar(Person person) {
+ final StringBuilder builder = new StringBuilder();
+ ObservableList eventList = person.getCalendar().getEventList();
+ builder.append(person.getName());
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+ for (int i = 0; i < eventList.size(); i++) {
+ Event event = eventList.get(i);
+ String eventDescription = event.getDescription().getDescription();
+ String startTime = formatter.format(event.getEventPeriod().getStart());
+ String endTime = formatter.format(event.getEventPeriod().getEnd());
+ builder.append(";\n")
+ .append(eventDescription)
+ .append(" ")
+ .append(startTime)
+ .append(" ")
+ .append(endTime);
+ }
+ return builder.toString();
+ }
}
diff --git a/src/main/java/seedu/address/logic/commands/AddContactEventCommand.java b/src/main/java/seedu/address/logic/commands/AddContactEventCommand.java
new file mode 100644
index 00000000000..eba985579fe
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/AddContactEventCommand.java
@@ -0,0 +1,98 @@
+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_EVENT_DESCRIPTION;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME;
+
+import java.util.List;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.logic.Messages;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.event.Event;
+import seedu.address.model.person.Person;
+
+/**
+ * Adds an event to the calendar of an existing person in the address book.
+ */
+public class AddContactEventCommand extends Command {
+
+ public static final String COMMAND_WORD = "addContactEvent";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an event to the calendar of the person "
+ + "identified by the index number used in the displayed person list.\n"
+ + "Parameters: INDEX (must be a positive integer) "
+ + PREFIX_EVENT_DESCRIPTION + "DESCRIPTION "
+ + PREFIX_EVENT_START_DATE_TIME + "START DATE AND TIME "
+ + PREFIX_EVENT_END_DATE_TIME + "END DATE AND TIME...\n"
+ + "Example: " + COMMAND_WORD + " 1 "
+ + PREFIX_EVENT_DESCRIPTION + "Nap "
+ + PREFIX_EVENT_START_DATE_TIME + "2024-01-01 12:00 "
+ + PREFIX_EVENT_END_DATE_TIME + "2024-01-01 18:00";
+
+ public static final String MESSAGE_ADD_EVENT_TO_PERSON_SUCCESS = "New event added to %s: %2$s";
+ public static final String MESSAGE_EVENT_CONFLICT = "This event is conflicting with another event";
+
+ private final Index index;
+ private final Event event;
+
+ /**
+ * @param index of the person in the filtered person list to add an event to
+ * @param event event to be added to the person
+ */
+ public AddContactEventCommand(Index index, Event event) {
+ requireAllNonNull(index, event);
+
+ this.index = index;
+ this.event = event;
+ }
+
+ @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);
+ }
+
+ // Gets the person which the event should be added to
+ Person personToEdit = lastShownList.get(index.getZeroBased());
+ if (!personToEdit.canAddEvent(event)) {
+ throw new CommandException(MESSAGE_EVENT_CONFLICT);
+ }
+
+ personToEdit.addEvent(event);
+
+ return new CommandResult(String.format(MESSAGE_ADD_EVENT_TO_PERSON_SUCCESS,
+ personToEdit.getName(), Messages.format(event)));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof AddContactEventCommand)) {
+ return false;
+ }
+
+ AddContactEventCommand otherAddContactEventCommand = (AddContactEventCommand) other;
+ return index.equals(otherAddContactEventCommand.index)
+ && event.equals(otherAddContactEventCommand.event);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("index", index)
+ .add("event", event)
+ .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..68b9865d3b4
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/AddEventCommand.java
@@ -0,0 +1,80 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME;
+
+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;
+
+/**
+ * Adds an event to the calendar.
+ */
+public class AddEventCommand extends Command {
+
+ public static final String COMMAND_WORD = "addEvent";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an event to the calendar. "
+ + "Parameters: "
+ + PREFIX_EVENT_DESCRIPTION + "DESCRIPTION "
+ + PREFIX_EVENT_START_DATE_TIME + "START DATE AND TIME "
+ + PREFIX_EVENT_END_DATE_TIME + "END DATE AND TIME...\n"
+ + "Example: " + COMMAND_WORD + " "
+ + PREFIX_EVENT_DESCRIPTION + "Nap "
+ + PREFIX_EVENT_START_DATE_TIME + "2024-01-01 12:00 "
+ + PREFIX_EVENT_END_DATE_TIME + "2024-01-01 18:00";
+
+ public static final String MESSAGE_SUCCESS = "New event added: %1$s";
+
+ public static final String MESSAGE_EVENT_CONFLICT = "This event is conflicting with another event";
+
+ private final Event toAdd;
+
+ /**
+ * Creates an AddEventCommand to add the event into the calendar.
+ *
+ * @param event event to be added.
+ */
+ public AddEventCommand(Event event) {
+ requireNonNull(event);
+ toAdd = event;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+
+ if (!model.canAddEvent(toAdd)) {
+ throw new CommandException(MESSAGE_EVENT_CONFLICT);
+ }
+
+ model.addEvent(toAdd);
+ return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd)));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof AddEventCommand)) {
+ return false;
+ }
+
+ AddEventCommand otherAddCommand = (AddEventCommand) other;
+ return toAdd.equals(otherAddCommand.toAdd);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("toAdd", toAdd)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/AddTaskCommand.java b/src/main/java/seedu/address/logic/commands/AddTaskCommand.java
new file mode 100644
index 00000000000..e97b218633f
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/AddTaskCommand.java
@@ -0,0 +1,80 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME;
+
+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.task.Task;
+import seedu.address.model.task.exceptions.DuplicateTaskException;
+
+/**
+ * Adds a Task to the TaskList
+ */
+public class AddTaskCommand extends Command {
+
+ public static final String COMMAND_WORD = "addTask";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a task to the task list. "
+ + "Parameters: "
+ + PREFIX_EVENT_DESCRIPTION + "DESCRIPTION "
+ + "[" + PREFIX_EVENT_END_DATE_TIME + "DEADLINE]\n"
+ + "Example: " + COMMAND_WORD + " "
+ + PREFIX_EVENT_DESCRIPTION + "Caffeinate "
+ + PREFIX_EVENT_END_DATE_TIME + "2024-01-01 08:00";
+
+ public static final String MESSAGE_SUCCESS = "New task added: %1$s";
+ public static final String MESSAGE_DUPLICATE_TASK = "This task already exists!\n"
+ + "Duplicate task: %s";
+ public static final String MESSAGE_INVALID_DESCRIPTION = "Description cannot be empty!\n%s";
+
+ private final Task toAdd;
+
+ /**
+ * Constructs an AddTaskCommand to add a task into the task list.
+ * @param task the task to add.
+ */
+ public AddTaskCommand(Task task) {
+ requireNonNull(task);
+
+ toAdd = task;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+
+ try {
+ model.addTask(toAdd);
+ } catch (DuplicateTaskException e) {
+ throw new CommandException(String.format(MESSAGE_DUPLICATE_TASK, Messages.format(toAdd)));
+ }
+
+ return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd)));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof AddTaskCommand)) {
+ return false;
+ }
+
+ AddTaskCommand otherAddTaskCommand = (AddTaskCommand) other;
+ return toAdd.equals(otherAddTaskCommand.toAdd);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("taskToAdd", toAdd)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/ClearEventsCommand.java b/src/main/java/seedu/address/logic/commands/ClearEventsCommand.java
new file mode 100644
index 00000000000..b0523fc6ad3
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/ClearEventsCommand.java
@@ -0,0 +1,112 @@
+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_CONFIRMATION;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME;
+
+import java.util.List;
+
+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.EventPeriod;
+
+/**
+ * Deletes all events within a time range in the calendar.
+ * Also uses a confirmation check to ensure that events are not accidentally deleted
+ */
+public class ClearEventsCommand extends Command {
+ public static final String COMMAND_WORD = "clearEvents";
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes events within a time range from the calendar. "
+ + "Shows all events within the time range instead if confirmation is not present.\n"
+ + "Parameters: "
+ + PREFIX_EVENT_START_DATE_TIME + "START DATE AND TIME "
+ + PREFIX_EVENT_END_DATE_TIME + "END DATE AND TIME ["
+ + PREFIX_CONFIRMATION + "CONFIRMATION]\n"
+ + "Example: " + COMMAND_WORD + " "
+ + PREFIX_EVENT_START_DATE_TIME + "2024-01-01 12:00 "
+ + PREFIX_EVENT_END_DATE_TIME + "2024-01-01 18:00";
+
+ public static final String MESSAGE_SUCCESS = "The following events within the time period have been deleted:\n";
+ public static final String MESSAGE_NO_EVENTS = "There are no events within this time period.";
+ public static final String MESSAGE_WITHIN_RANGE = "The following events are within the time period:\n";
+ public static final String MESSAGE_ADD_CONFIRMATION = "Reenter the command with [c/CONFIRMED] at the end to "
+ + "confirm deletion.\n";
+ public static final String COMMAND_RESEND_FORMAT = "clearEvents ts/%s te/%s c/CONFIRMED";
+
+ private final EventPeriod clearPeriod;
+ private final boolean isConfirmed;
+
+ /**
+ * Creates a ClearEventsCommand to delete all events within a time range from the calendar.
+ *
+ * @param clearPeriod The time period for which to delete all events.
+ * @param isConfirmed A boolean to check if the deletion is confirmed by the user.
+ */
+ public ClearEventsCommand(EventPeriod clearPeriod, boolean isConfirmed) {
+ requireAllNonNull(clearPeriod, isConfirmed);
+ this.clearPeriod = clearPeriod;
+ this.isConfirmed = isConfirmed;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+
+ List eventsInRange = model.eventsInRange(clearPeriod);
+ if (eventsInRange.isEmpty()) {
+ throw new CommandException(MESSAGE_NO_EVENTS);
+ }
+ StringBuilder sb;
+
+ if (isConfirmed) {
+ sb = new StringBuilder(MESSAGE_SUCCESS);
+ } else {
+ sb = new StringBuilder(MESSAGE_WITHIN_RANGE);
+ }
+
+ int i = 1;
+ for (Event event:eventsInRange) {
+ sb.append(String.format("%d. %s\n", i, Messages.format(event)));
+ i++;
+ }
+ if (isConfirmed) {
+ model.deleteEventsInRange(clearPeriod);
+ } else {
+ sb.append(MESSAGE_ADD_CONFIRMATION);
+ sb.append(String.format(COMMAND_RESEND_FORMAT,
+ clearPeriod.getFormattedStart(),
+ clearPeriod.getFormattedEnd()));
+ }
+ return new CommandResult(sb.toString());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof ClearEventsCommand)) {
+ return false;
+ }
+
+ ClearEventsCommand otherClearEventsCommand = (ClearEventsCommand) other;
+ boolean periodIsEqual = clearPeriod.equals(otherClearEventsCommand.clearPeriod);
+ boolean confirmationIsEqual = isConfirmed == otherClearEventsCommand.isConfirmed;
+ return periodIsEqual && confirmationIsEqual;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("toClearWithin", clearPeriod)
+ .add("confirmed", isConfirmed)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java
index 249b6072d0d..ab929d41ae8 100644
--- a/src/main/java/seedu/address/logic/commands/CommandResult.java
+++ b/src/main/java/seedu/address/logic/commands/CommandResult.java
@@ -4,12 +4,14 @@
import java.util.Objects;
+import seedu.address.commons.core.index.Index;
import seedu.address.commons.util.ToStringBuilder;
/**
* Represents the result of a command execution.
*/
public class CommandResult {
+ private static final int INVALID_INDEX = -2;
private final String feedbackToUser;
@@ -19,13 +21,25 @@ public class CommandResult {
/** The application should exit. */
private final boolean exit;
+ /** The application should show the task list in the bottom list slot. */
+ private final boolean switchBottomList;
+
+ private final ViewEventsIndicator eventViewIndex;
+
+ /** Application should display calendar comparison result */
+ private final boolean showCalendarComparison;
+
/**
* Constructs a {@code CommandResult} with the specified fields.
*/
- public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) {
+ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, boolean showCalendarComparison,
+ boolean switchBottomList) {
this.feedbackToUser = requireNonNull(feedbackToUser);
this.showHelp = showHelp;
this.exit = exit;
+ this.showCalendarComparison = showCalendarComparison;
+ this.switchBottomList = switchBottomList;
+ this.eventViewIndex = new ViewEventsIndicator(INVALID_INDEX);
}
/**
@@ -33,7 +47,20 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) {
* and other fields set to their default value.
*/
public CommandResult(String feedbackToUser) {
- this(feedbackToUser, false, false);
+ this(feedbackToUser, false, false, false, false);
+ }
+
+ /**
+ * Constructs a {@code CommandResult} with the specified {@code feedbackToUser},
+ * {@code eventViewIndex}, and other fields set to their default value
+ */
+ public CommandResult(String feedbackToUser, Index eventViewIndex) {
+ this.feedbackToUser = feedbackToUser;
+ this.showHelp = false;
+ this.exit = false;
+ this.showCalendarComparison = false;
+ this.switchBottomList = false;
+ this.eventViewIndex = new ViewEventsIndicator(eventViewIndex.getOneBased());
}
public String getFeedbackToUser() {
@@ -48,6 +75,22 @@ public boolean isExit() {
return exit;
}
+ public boolean isSwitchBottomList() {
+ return switchBottomList;
+ }
+
+ public Index getEventViewIndex() {
+ return eventViewIndex.getIndex();
+ }
+
+ public boolean isViewEvents() {
+ return eventViewIndex.isViewEvents();
+ }
+
+ public boolean isShowCalendarComparison() {
+ return showCalendarComparison;
+ }
+
@Override
public boolean equals(Object other) {
if (other == this) {
@@ -62,12 +105,13 @@ public boolean equals(Object other) {
CommandResult otherCommandResult = (CommandResult) other;
return feedbackToUser.equals(otherCommandResult.feedbackToUser)
&& showHelp == otherCommandResult.showHelp
- && exit == otherCommandResult.exit;
+ && exit == otherCommandResult.exit
+ && switchBottomList == otherCommandResult.switchBottomList;
}
@Override
public int hashCode() {
- return Objects.hash(feedbackToUser, showHelp, exit);
+ return Objects.hash(feedbackToUser, showHelp, exit, switchBottomList);
}
@Override
@@ -76,7 +120,41 @@ public String toString() {
.add("feedbackToUser", feedbackToUser)
.add("showHelp", showHelp)
.add("exit", exit)
+ .add("switchBottomList", switchBottomList)
.toString();
}
+ /**
+ * An indicator class to indicate if an event list is being viewed.
+ */
+ public static class ViewEventsIndicator {
+ private static final int MINIMUM_INDEX = 1;
+ private final Index index;
+ private final boolean isViewEvents;
+
+ /**
+ * Constructs a valid indicator if the index is above 1 and an invalid one otherwise.
+ */
+ public ViewEventsIndicator(int index) {
+ if (index >= MINIMUM_INDEX) {
+ this.index = Index.fromOneBased(index);
+ isViewEvents = true;
+ } else {
+ this.index = Index.fromOneBased(MINIMUM_INDEX);
+ isViewEvents = false;
+ }
+ }
+
+ /**
+ * Checks if the result requires events to be viewed.
+ */
+ public boolean isViewEvents() {
+ return isViewEvents;
+ }
+
+ public Index getIndex() {
+ return index;
+ }
+ }
+
}
diff --git a/src/main/java/seedu/address/logic/commands/CompareCalendarByIndexCommand.java b/src/main/java/seedu/address/logic/commands/CompareCalendarByIndexCommand.java
new file mode 100644
index 00000000000..c00277bcfec
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/CompareCalendarByIndexCommand.java
@@ -0,0 +1,55 @@
+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.calendar.ReadOnlyCalendar;
+import seedu.address.model.person.Person;
+
+/**
+ * Command for comparing calendars of user and persons in address book.
+ */
+public class CompareCalendarByIndexCommand extends Command {
+ public static final String COMMAND_WORD = "compareCalendars";
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": compare calendars with specified people index. "
+ + "Parameters: "
+ + "NONE/ONE/MULTIPLE VALID PERSON INDEX \n"
+ + "Example: " + COMMAND_WORD + " "
+ + "1 2 3";
+
+ public static final String MESSAGE_SUCCESS = "Displaying available time periods";
+ private final List indexList;
+
+ /**
+ * Create a new CompareCalendarCommand with a list of index of persons to compare with.
+ *
+ * @param indexList list of index of persons to compare with.
+ */
+ public CompareCalendarByIndexCommand(List indexList) {
+ this.indexList = indexList;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredPersonList();
+
+ if (indexList.stream().map(Index::getZeroBased)
+ .anyMatch(zeroBasedIndex -> zeroBasedIndex >= lastShownList.size())) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ ReadOnlyCalendar combinedCalendar = indexList.stream().map(Index::getZeroBased).map(lastShownList::get)
+ .map(Person::getReadOnlyCalendar)
+ .reduce(model.getCalendar(), ReadOnlyCalendar::getCombinedCalendar);
+
+ model.setComparisonCalendar(combinedCalendar);
+
+ return new CommandResult(MESSAGE_SUCCESS, false, false, true, false);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/CompareCalendarByTagCommand.java b/src/main/java/seedu/address/logic/commands/CompareCalendarByTagCommand.java
new file mode 100644
index 00000000000..7aee4efa053
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/CompareCalendarByTagCommand.java
@@ -0,0 +1,45 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.List;
+
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.person.Person;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Command to compare calendars of user with people in the calendar with some collection of tags.
+ */
+public class CompareCalendarByTagCommand extends Command {
+ public static final String COMMAND_WORD = "compareGroupCalendars";
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": compare calendars within people with specified tags. "
+ + "Parameters: "
+ + "NONE/ONE/MULTIPLE VALID TAGS \n"
+ + "Example: " + COMMAND_WORD + " "
+ + "class friend";
+
+ public static final String MESSAGE_SUCCESS = "Displaying available time periods";
+
+ private final List tagList;
+
+ public CompareCalendarByTagCommand(List tagList) {
+ this.tagList = tagList;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredPersonList();
+
+ ReadOnlyCalendar comparisonCalendar = lastShownList.stream().filter(person -> {
+ return tagList.stream().anyMatch(person::hasTag);
+ }).map(Person::getReadOnlyCalendar).reduce(model.getCalendar(), ReadOnlyCalendar::getCombinedCalendar);
+
+ model.setComparisonCalendar(comparisonCalendar);
+
+ return new CommandResult(MESSAGE_SUCCESS, false, false, true, false);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java
index 1135ac19b74..f72d7081313 100644
--- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java
+++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java
@@ -12,7 +12,7 @@
import seedu.address.model.person.Person;
/**
- * Deletes a person identified using it's displayed index from the address book.
+ * Deletes a person identified using its displayed index from the address book.
*/
public class DeleteCommand extends Command {
diff --git a/src/main/java/seedu/address/logic/commands/DeleteContactEventCommand.java b/src/main/java/seedu/address/logic/commands/DeleteContactEventCommand.java
new file mode 100644
index 00000000000..671ef726831
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/DeleteContactEventCommand.java
@@ -0,0 +1,97 @@
+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_EVENT_START_DATE_TIME;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.logic.Messages;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.event.Event;
+import seedu.address.model.person.Person;
+
+/**
+ * Delete an event from the calendar of an existing person in the address book.
+ */
+public class DeleteContactEventCommand extends Command {
+
+ public static final String COMMAND_WORD = "deleteContactEvent";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes an event from the calendar of the person "
+ + "identified by the index number used in the displayed person list.\n"
+ + "Parameters: INDEX (must be a positive integer) "
+ + PREFIX_EVENT_START_DATE_TIME + "ANY TIME WITHIN EVENT DURATION \n"
+ + "Example: " + COMMAND_WORD + " 1 "
+ + PREFIX_EVENT_START_DATE_TIME + "2024-01-01 12:00 ";
+
+ public static final String MESSAGE_DELETE_EVENT_FROM_PERSON_SUCCESS = "Event deleted from %s: %2$s";
+ public static final String MESSAGE_NO_EVENT = "There is no valid existing event at this timing.";
+
+ private final Index index;
+ private final LocalDateTime eventTime;
+
+ /**
+ * @param index of the person in the filtered person list to delete an event from.
+ * @param eventTime time of the event to delete.
+ */
+ public DeleteContactEventCommand(Index index, LocalDateTime eventTime) {
+ requireAllNonNull(index, eventTime);
+
+ this.index = index;
+ this.eventTime = eventTime;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredPersonList();
+
+ //Obtain the calendar of the person and tries to delete an event from the person's calendar
+ Person personToEdit;
+ try {
+ personToEdit = lastShownList.get(index.getZeroBased());
+ } catch (IndexOutOfBoundsException e) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ Event toDelete;
+
+ try {
+ toDelete = personToEdit.findEvent(eventTime);
+ personToEdit.deleteEvent(eventTime);
+ } catch (Exception e) {
+ throw new CommandException(MESSAGE_NO_EVENT);
+ }
+ return new CommandResult(String.format(MESSAGE_DELETE_EVENT_FROM_PERSON_SUCCESS,
+ personToEdit.getName(), Messages.format(toDelete)));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof DeleteContactEventCommand)) {
+ return false;
+ }
+
+ DeleteContactEventCommand otherDeleteContactEventCommand = (DeleteContactEventCommand) other;
+ return index.equals(otherDeleteContactEventCommand.index)
+ && eventTime.equals(otherDeleteContactEventCommand.eventTime);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("index", index)
+ .add("eventTime", eventTime)
+ .toString();
+ }
+}
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..c2cfed3f1c4
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/DeleteEventCommand.java
@@ -0,0 +1,74 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.LocalDateTime;
+
+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.exceptions.EventNotFoundException;
+
+/**
+ * Deletes an event from the calendar.
+ */
+public class DeleteEventCommand extends Command {
+ public static final String COMMAND_WORD = "deleteEvent";
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes an event from the calendar. "
+ + "Parameters: "
+ + "ANY TIME WITHIN EVENT DURATION \n"
+ + "Example: " + COMMAND_WORD + " "
+ + "2024-01-01 12:00 ";
+
+ public static final String MESSAGE_SUCCESS = "Event deleted: %1$s";
+ public static final String MESSAGE_NO_EVENT = "There is no valid existing event at this timing.";
+ private final LocalDateTime eventTime;
+
+ /**
+ * Creates a DeleteEventCommand to delete an event from the calendar.
+ *
+ * @param eventTime time of the event to delete.
+ */
+ public DeleteEventCommand(LocalDateTime eventTime) {
+ requireNonNull(eventTime);
+ this.eventTime = eventTime;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+
+ Event toDelete;
+ try {
+ toDelete = model.findEventAt(eventTime);
+ model.deleteEventAt(eventTime);
+ } catch (EventNotFoundException e) {
+ throw new CommandException(MESSAGE_NO_EVENT);
+ }
+ return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toDelete)));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof DeleteEventCommand)) {
+ return false;
+ }
+
+ DeleteEventCommand otherDeleteCommand = (DeleteEventCommand) other;
+ return eventTime.equals(otherDeleteCommand.eventTime);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("toDeleteAt", eventTime)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/DeleteTaskCommand.java b/src/main/java/seedu/address/logic/commands/DeleteTaskCommand.java
new file mode 100644
index 00000000000..4b656880a7d
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/DeleteTaskCommand.java
@@ -0,0 +1,67 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+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.task.Task;
+
+/**
+ * Deletes a task identified using it's displayed index from the task list.
+ */
+public class DeleteTaskCommand extends Command {
+
+ public static final String COMMAND_WORD = "deleteTask";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Deletes the task identified by the index number used in the displayed task list.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "Example: " + COMMAND_WORD + " 1";
+
+ public static final String MESSAGE_DELETE_TASK_SUCCESS = "Deleted Task: %1$s";
+ public static final String MESSAGE_INVALID_INDEX = "The index provided is invalid!";
+
+ private final Index targetIndex;
+
+ public DeleteTaskCommand(Index targetIndex) {
+ this.targetIndex = targetIndex;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ Task toDelete;
+
+ try {
+ toDelete = model.deleteTask(targetIndex.getZeroBased());
+ return new CommandResult(String.format(MESSAGE_DELETE_TASK_SUCCESS, Messages.format(toDelete)));
+ } catch (IndexOutOfBoundsException e) {
+ throw new CommandException(MESSAGE_INVALID_INDEX);
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof DeleteTaskCommand)) {
+ return false;
+ }
+
+ DeleteTaskCommand otherDeleteTaskCommand = (DeleteTaskCommand) other;
+ return targetIndex.equals(otherDeleteTaskCommand.targetIndex);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("targetIndex", targetIndex)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java
index 4b581c7331e..c50c8eb5d11 100644
--- a/src/main/java/seedu/address/logic/commands/EditCommand.java
+++ b/src/main/java/seedu/address/logic/commands/EditCommand.java
@@ -21,6 +21,7 @@
import seedu.address.logic.Messages;
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.model.Model;
+import seedu.address.model.calendar.UniMateCalendar;
import seedu.address.model.person.Address;
import seedu.address.model.person.Email;
import seedu.address.model.person.Name;
@@ -100,8 +101,9 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript
Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail());
Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress());
Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags());
+ UniMateCalendar calendar = personToEdit.getCalendar();
- return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags);
+ return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, calendar);
}
@Override
diff --git a/src/main/java/seedu/address/logic/commands/EditContactEventCommand.java b/src/main/java/seedu/address/logic/commands/EditContactEventCommand.java
new file mode 100644
index 00000000000..79ae7d57458
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/EditContactEventCommand.java
@@ -0,0 +1,271 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME;
+import static seedu.address.logic.parser.ParserUtil.parseDateTimeNonNull;
+import static seedu.address.logic.parser.ParserUtil.parseDateTimeToString;
+import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import javafx.collections.ObservableList;
+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.calendar.UniMateCalendar;
+import seedu.address.model.event.Event;
+import seedu.address.model.event.EventDescription;
+import seedu.address.model.event.EventPeriod;
+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's calendar in the address book.
+ */
+public class EditContactEventCommand extends Command {
+
+ public static final String COMMAND_WORD = "editContactEvent";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the identified person's calendar "
+ + "by the index number used in the displayed person list. "
+ + "Existing values will be overwritten by the input values.\n"
+ + "Parameters: PERSON_INDEX (must be a positive integer) "
+ + "EVENT_INDEX (must be a positive integer)"
+ + "[" + PREFIX_EVENT_DESCRIPTION + "DESCRIPTION] "
+ + "[" + PREFIX_EVENT_START_DATE_TIME + "START_DATE] "
+ + "[" + PREFIX_EVENT_END_DATE_TIME + "END_DATE] \n"
+ + "Example: " + COMMAND_WORD + " 1 " + "1 "
+ + PREFIX_EVENT_DESCRIPTION + "Nap "
+ + PREFIX_EVENT_START_DATE_TIME + "2023-10-10 10:00 "
+ + PREFIX_EVENT_END_DATE_TIME + "2023-10-10 15:00 ";
+
+ public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s";
+ public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided.";
+ public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book.";
+ public static final String MESSAGE_WRONG_TIME = "Both the start and end time must be edited!";
+ public static final String INVALID_EVENT_INDEX = "Invalid Event Index Provided";
+ private final Index personIndex;
+ private final Index eventIndex;
+ private final EditEventDescriptor editEventDescriptor;
+
+ /**
+ * @param indexArrayList an ArrayList consisting of two elements, that is the Index of target person and
+ * the index of the target event.
+ * @param editEventDescriptor details to edit the calendar with
+ */
+ public EditContactEventCommand(ArrayList indexArrayList, EditEventDescriptor editEventDescriptor) {
+ requireNonNull(indexArrayList.get(0));
+ requireNonNull(indexArrayList.get(1));
+ requireNonNull(editEventDescriptor);
+ this.personIndex = indexArrayList.get(0);
+ this.eventIndex = indexArrayList.get(1);
+ this.editEventDescriptor = editEventDescriptor;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredPersonList();
+
+ if (personIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ Person personToEdit = lastShownList.get(personIndex.getZeroBased());
+ Person editedPerson = createPersonsEditedEvent(eventIndex, personToEdit, editEventDescriptor);
+
+ 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.formatCalendar(editedPerson)));
+ }
+
+ /**
+ * Creates and returns a {@code Person} with the details of {@code personToEdit}, editing the event with index
+ * {@code eventIndex} with {@code editEventDescriptor}.
+ */
+ private static Person createPersonsEditedEvent(Index eventIndex,
+ Person personToEdit, EditEventDescriptor editEventDescriptor)
+ throws CommandException {
+ assert personToEdit != null;
+
+ Name updatedName = personToEdit.getName();
+ Phone updatedPhone = personToEdit.getPhone();
+ Email updatedEmail = personToEdit.getEmail();
+ Address updatedAddress = personToEdit.getAddress();
+ Set updatedTags = personToEdit.getTags();
+ UniMateCalendar calendar = personToEdit.getCalendar();
+ List eventList = updateEventList(calendar, eventIndex, editEventDescriptor);
+ UniMateCalendar updatedCalendar = new UniMateCalendar();
+ for (Event e: eventList) {
+ updatedCalendar.addEvent(e);
+ }
+
+ return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, updatedCalendar);
+ }
+
+ /**
+ * Updates an event by its {@code eventIndex} in the given {@code calendar} with the new information provided in
+ * the {@code editEventDescriptor}.
+ *
+ * @return The updated event list.
+ */
+ public static List updateEventList(UniMateCalendar calendar, Index eventIndex,
+ EditEventDescriptor editEventDescriptor) throws CommandException {
+ ObservableList eventList = calendar.getEventManager().asUnmodifiableObservableList();
+ Event updateEvent;
+ try {
+ updateEvent = eventList.get(eventIndex.getZeroBased());
+ } catch (Exception e) {
+ throw new CommandException(INVALID_EVENT_INDEX);
+ }
+ EventPeriod updatePeriod = updateEvent.getEventPeriod();
+ LocalDateTime newStartDateTime = editEventDescriptor.getStart().orElse(updatePeriod.getStart());
+ LocalDateTime newEndDateTime = editEventDescriptor.getEnd().orElse(updatePeriod.getEnd());
+ EventDescription newEventDescription = editEventDescriptor.getEventDescription()
+ .orElse(updateEvent.getDescription());
+ String stringStartDateTime = parseDateTimeToString(newStartDateTime);
+ String stringEndDateTime = parseDateTimeToString(newEndDateTime);
+ EventPeriod newEventPeriod = new EventPeriod(stringStartDateTime, stringEndDateTime);
+ Event updatedEvent = new Event(newEventDescription, newEventPeriod);
+ eventList.set(eventIndex.getZeroBased(), updatedEvent);
+ return eventList;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof EditContactEventCommand)) {
+ return false;
+ }
+
+ EditContactEventCommand otherEditCommand = (EditContactEventCommand) other;
+ return personIndex.equals(otherEditCommand.personIndex)
+ && eventIndex.equals(otherEditCommand.eventIndex)
+ && editEventDescriptor.equals(otherEditCommand.editEventDescriptor);
+ }
+
+ /**
+ * Stores the details to edit the person with. Each non-empty field value will replace the
+ * corresponding field value of the calendar of the person.
+ */
+ public static class EditEventDescriptor {
+ private UniMateCalendar calendar;
+ private Event event;
+ private EventDescription eventDescription;
+ private EventPeriod eventPeriod;
+ private LocalDateTime start;
+ private LocalDateTime end;
+
+ public EditEventDescriptor() {}
+
+ /**
+ * Copy constructor.
+ * A defensive copy of {@code tags} is used internally.
+ */
+ public EditEventDescriptor(EditEventDescriptor toCopy) {
+ setEventPeriod(toCopy.eventPeriod);
+ setEventDescription(toCopy.eventDescription);
+ }
+
+ /**
+ * Returns true if at least one field is edited.
+ */
+ public boolean isAnyFieldEdited() {
+ return CollectionUtil.isAnyNonNull(start, end, eventDescription);
+ }
+
+ public void setEventDescription(EventDescription eventDescription) {
+ this.eventDescription = eventDescription;
+ }
+
+ public Optional getEventDescription() {
+ return Optional.ofNullable(eventDescription);
+ }
+
+ public void setEventPeriod(EventPeriod eventPeriod) {
+ this.eventPeriod = eventPeriod;
+ setStart(eventPeriod.getStart());
+ setEnd(eventPeriod.getEnd());
+ }
+
+ public Optional getEventPeriod() {
+ return Optional.ofNullable(eventPeriod);
+ }
+ public void setStart(String startString) {
+ LocalDateTime start = parseDateTimeNonNull(startString);
+ this.start = start;
+ }
+
+ public void setStart(LocalDateTime start) {
+ this.start = start;
+ }
+
+ public Optional getStart() {
+ return Optional.ofNullable(start);
+ }
+
+ public void setEnd(String endString) {
+ LocalDateTime end = parseDateTimeNonNull(endString);
+ this.end = end;
+ }
+
+ public void setEnd(LocalDateTime end) {
+ this.end = end;
+ }
+
+ public Optional getEnd() {
+ return Optional.ofNullable(end);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof EditEventDescriptor)) {
+ return false;
+ }
+
+ EditEventDescriptor otherEditEventDescriptor = (EditEventDescriptor) other;
+ return Objects.equals(eventDescription, otherEditEventDescriptor.eventDescription)
+ && Objects.equals(start, otherEditEventDescriptor.start)
+ && Objects.equals(end, otherEditEventDescriptor.end);
+ }
+
+ @Override
+ public String toString() {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+ return new ToStringBuilder("")
+ .add("eventDescription", eventDescription)
+ .add("start", parseDateTimeToString(eventPeriod.getStart()))
+ .add("end", parseDateTimeToString(eventPeriod.getEnd()))
+ .toString();
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java
index 3dd85a8ba90..7874f9ccfc1 100644
--- a/src/main/java/seedu/address/logic/commands/ExitCommand.java
+++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java
@@ -13,7 +13,7 @@ public class ExitCommand extends Command {
@Override
public CommandResult execute(Model model) {
- return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true);
+ return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, false, false);
}
}
diff --git a/src/main/java/seedu/address/logic/commands/FilterCommand.java b/src/main/java/seedu/address/logic/commands/FilterCommand.java
new file mode 100644
index 00000000000..e586687e293
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/FilterCommand.java
@@ -0,0 +1,237 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.nonNull;
+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 java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import seedu.address.commons.util.CollectionUtil;
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.logic.Messages;
+import seedu.address.model.Model;
+import seedu.address.model.person.Person;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Filters contact list to display contacts matching specified criteria.
+ */
+public class FilterCommand extends Command {
+ public static final String COMMAND_WORD = "filter";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose attributes contain any of "
+ + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n"
+ + "Parameters: "
+ + PREFIX_NAME + "NAME "
+ + PREFIX_PHONE + "PHONE "
+ + PREFIX_EMAIL + "EMAIL "
+ + PREFIX_ADDRESS + "ADDRESS "
+ + "[" + PREFIX_TAG + "TAG]...\n"
+ + "Example: " + COMMAND_WORD + " "
+ + PREFIX_NAME + "Hugh "
+ + PREFIX_PHONE + "90 "
+ + PREFIX_EMAIL + "@u.nus.edu "
+ + PREFIX_ADDRESS + "123, Tengah Ave 6, #69-420 "
+ + PREFIX_TAG + "CS2103 "
+ + PREFIX_TAG + "CSGod";
+ public static final String CONTACTS_NOT_FILTERED = String.format("%s \n%s",
+ "At least one parameter is required to filter contacts.", MESSAGE_USAGE);
+ private final PersonFilter personFilter;
+
+ /**
+ * Filters out all persons that do not match the filter criteria specified.
+ * Field matching is case-insensitive
+ *
+ * @param personFilter Details to filter people with
+ */
+ public FilterCommand(PersonFilter personFilter) {
+ requireNonNull(personFilter);
+ this.personFilter = personFilter;
+ }
+
+ @Override
+ public CommandResult execute(Model model) {
+ requireNonNull(model);
+ model.updateFilteredPersonList(personFilter::matchesFilter);
+ return new CommandResult(
+ String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size()));
+ }
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof FilterCommand)) {
+ return false;
+ }
+
+ FilterCommand otherFilterCommand = (FilterCommand) other;
+ return personFilter.equals(otherFilterCommand.personFilter);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("personFilter", personFilter)
+ .toString();
+ }
+
+ /**
+ * Stores the details to filter the contacts by. Contacts will be filtered by each non-empty field .
+ */
+ public static class PersonFilter {
+ private String name;
+ private String phone;
+ private String email;
+ private String address;
+ private Set tags;
+
+ public PersonFilter() {}
+
+ /**
+ * Creates a copy of a filter with the same parameters as another filter.
+ *
+ * @param toCopy The other filter to create a copy of.
+ */
+ public PersonFilter(PersonFilter 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(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getAddress() {
+ return 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 Set getTags() {
+ return (tags != null) ? Collections.unmodifiableSet(tags) : null;
+ }
+
+ /**
+ * Predicate to test if a person passes the filter by matching all parameters.
+ *
+ * @param person The person to be tested.
+ * @return a boolean representing whether the person passed the filter.
+ */
+ public boolean matchesFilter(Person person) {
+ if (nonNull(name) && !person.getName().fullName.toLowerCase().contains(name.toLowerCase())) {
+ return false;
+ }
+ if (nonNull(phone) && !person.getPhone().value.contains(phone)) {
+ return false;
+ }
+ if (nonNull(email) && !person.getEmail().value.toLowerCase().contains(email.toLowerCase())) {
+ return false;
+ }
+ if (nonNull(address) && !person.getAddress().value.toLowerCase().contains(address.toLowerCase())) {
+ return false;
+ }
+ if (nonNull(tags) && !tags.isEmpty()) {
+ Stream tagsToCheck = tags.stream();
+ Stream currentTags = person.getTags().stream();
+ return tagsToCheck.allMatch(x -> currentTags.anyMatch(y -> y.tagName.contains(x.tagName)));
+ }
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof PersonFilter)) {
+ return false;
+ }
+
+ PersonFilter otherPersonFilter = (PersonFilter) other;
+ return isEqual(name, otherPersonFilter.getName())
+ && isEqual(phone, otherPersonFilter.getPhone())
+ && isEqual(email, otherPersonFilter.getEmail())
+ && isEqual(address, otherPersonFilter.getAddress())
+ && isEqual(tags, otherPersonFilter.getTags());
+ }
+
+ private boolean isEqual(Object first, Object second) {
+ if (Objects.isNull(first)) {
+ return Objects.isNull(second);
+ } else if (Objects.isNull(second)) {
+ return false;
+ } else {
+ return first.equals(second);
+ }
+ }
+
+ @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/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java
index bf824f91bd0..492a3633fbd 100644
--- a/src/main/java/seedu/address/logic/commands/HelpCommand.java
+++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java
@@ -16,6 +16,6 @@ public class HelpCommand extends Command {
@Override
public CommandResult execute(Model model) {
- return new CommandResult(SHOWING_HELP_MESSAGE, true, false);
+ return new CommandResult(SHOWING_HELP_MESSAGE, true, false, false, false);
}
}
diff --git a/src/main/java/seedu/address/logic/commands/SortCommand.java b/src/main/java/seedu/address/logic/commands/SortCommand.java
new file mode 100644
index 00000000000..6ad1dde88f6
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/SortCommand.java
@@ -0,0 +1,47 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+import seedu.address.model.Model;
+import seedu.address.model.person.Person;
+import seedu.address.model.person.comparer.SortComparator;
+
+/**
+ * Sorts the persons in the address book by the given field.
+ */
+public class SortCommand extends Command {
+
+ public static final String COMMAND_WORD = "sort";
+ public static final String SORTBY_KEYWORD1 = "/byaddress";
+ public static final String SORTBY_KEYWORD2 = "/byemail";
+ public static final String SORTBY_KEYWORD3 = "/byname";
+ public static final String SORTBY_KEYWORD4 = "/byphone";
+ public static final String REVERSE_KEYWORD = "/reverse";
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sorts all persons in UniMate "
+ + "by the ascending or descending order of specified attribute. \n"
+ + "Example: " + COMMAND_WORD + " " + SORTBY_KEYWORD1 + " " + REVERSE_KEYWORD;
+
+ public static final String MESSAGE_SUCCESS = "Sorted all persons by specified order";
+ private Comparator personComparator;
+
+ /**
+ * Constructor for SortCommand, creates a comparator to sort by the specified field and order.
+ */
+ public SortCommand(ArrayList sortComparatorList) {
+ SortComparator sortComparator = sortComparatorList.get(0);
+ boolean isReverse = sortComparator.getIsReverse();
+ this.personComparator = isReverse
+ ? sortComparator.reversed()
+ : sortComparator;
+ }
+
+ @Override
+ public CommandResult execute(Model model) {
+ requireNonNull(model);
+ model.sortPersonList(personComparator);
+ return new CommandResult(MESSAGE_SUCCESS);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/SortTasksCommand.java b/src/main/java/seedu/address/logic/commands/SortTasksCommand.java
new file mode 100644
index 00000000000..f75012a9f7b
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/SortTasksCommand.java
@@ -0,0 +1,64 @@
+package seedu.address.logic.commands;
+
+import static java.util.Objects.requireNonNull;
+
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.logic.commands.exceptions.CommandException;
+import seedu.address.model.Model;
+import seedu.address.model.task.exceptions.InvalidSortingOrderException;
+
+/**
+ * Changes the sorting order of tasks in the task manager.
+ */
+public class SortTasksCommand extends Command {
+ public static final String COMMAND_WORD = "sortTasks";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD + ": Changes the way tasks in the task list are sorted."
+ + "Parameters: Description OR Deadline\n"
+ + "Example: " + COMMAND_WORD + " Description";
+
+ public static final String MESSAGE_SUCCESS = "Sorting order changed to %s.";
+
+ public static final String MESSAGE_INVALID_TYPE = "Sorting order provided is invalid!\n"
+ + "Sorting orders available: DESCRIPTION/DEADLINE.";
+
+ private final String comparatorType;
+
+ public SortTasksCommand(String comparatorType) {
+ this.comparatorType = comparatorType;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+
+ try {
+ model.sortTasksBy(comparatorType);
+ } catch (InvalidSortingOrderException e) {
+ throw new CommandException(MESSAGE_INVALID_TYPE);
+ }
+ return new CommandResult(String.format(MESSAGE_SUCCESS, comparatorType));
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof SortTasksCommand)) {
+ return false;
+ }
+
+ SortTasksCommand otherSortTasksCommand = (SortTasksCommand) other;
+ return comparatorType.equals((otherSortTasksCommand).comparatorType);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("comparatorType", comparatorType)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/SwitchListCommand.java b/src/main/java/seedu/address/logic/commands/SwitchListCommand.java
new file mode 100644
index 00000000000..ef805170d25
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/SwitchListCommand.java
@@ -0,0 +1,18 @@
+package seedu.address.logic.commands;
+
+import seedu.address.model.Model;
+
+/**
+ * Switches the bottom list between the task list and the event list.
+ */
+public class SwitchListCommand extends Command {
+
+ public static final String COMMAND_WORD = "switchList";
+
+ public static final String MESSAGE_SWITCH_ACKNOWLEDGEMENT = "Switching list.";
+
+ @Override
+ public CommandResult execute(Model model) {
+ return new CommandResult(MESSAGE_SWITCH_ACKNOWLEDGEMENT, false, false, false, true);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/commands/ViewContactEventsCommand.java b/src/main/java/seedu/address/logic/commands/ViewContactEventsCommand.java
new file mode 100644
index 00000000000..c7e0017bed3
--- /dev/null
+++ b/src/main/java/seedu/address/logic/commands/ViewContactEventsCommand.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.person.Person;
+
+/**
+ * Creates a pop-up window to view the event list of a person using its displayed index from the address book.
+ */
+public class ViewContactEventsCommand extends Command {
+ public static final String COMMAND_WORD = "viewContactEvents";
+
+ public static final String MESSAGE_USAGE = COMMAND_WORD
+ + ": Creates a window containing the event list of a person identified by their index number.\n"
+ + "Parameters: INDEX (must be a positive integer)\n"
+ + "Example: " + COMMAND_WORD + " 1";
+
+ public static final String MESSAGE_VIEW_EVENTS_SUCCESS = "Viewing event list.";
+
+ private final Index targetIndex;
+
+ public ViewContactEventsCommand(Index targetIndex) {
+ this.targetIndex = targetIndex;
+ }
+
+ @Override
+ public CommandResult execute(Model model) throws CommandException {
+ requireNonNull(model);
+ List lastShownList = model.getFilteredPersonList();
+
+ if (targetIndex.getZeroBased() >= lastShownList.size()) {
+ throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX);
+ }
+
+ return new CommandResult(MESSAGE_VIEW_EVENTS_SUCCESS, targetIndex);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof ViewContactEventsCommand)) {
+ return false;
+ }
+
+ ViewContactEventsCommand otherViewContactEventsCommand = (ViewContactEventsCommand) other;
+ return targetIndex.equals(otherViewContactEventsCommand.targetIndex);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/AddContactEventCommandParser.java b/src/main/java/seedu/address/logic/parser/AddContactEventCommandParser.java
new file mode 100644
index 00000000000..7835c800288
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/AddContactEventCommandParser.java
@@ -0,0 +1,72 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME;
+import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE;
+
+import java.util.stream.Stream;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.AddContactEventCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.event.Event;
+import seedu.address.model.event.EventDescription;
+import seedu.address.model.event.EventPeriod;
+
+/**
+ * Parses input arguments and creates a new AddContactEventCommand object
+ */
+public class AddContactEventCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the AddContactEventCommand
+ * and returns an AddContactEventCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public AddContactEventCommand parse(String args) throws ParseException {
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args, PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME,
+ PREFIX_EVENT_END_DATE_TIME);
+
+ Index index;
+ try {
+ index = ParserUtil.parseIndex(argMultimap.getPreamble());
+ } catch (ParseException pe) {
+ if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) {
+ throw new ParseException(MESSAGE_INTEGER_OVERFLOW);
+ } else {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddContactEventCommand.MESSAGE_USAGE), pe);
+ }
+ }
+
+ if (!arePrefixesPresent(argMultimap, PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME,
+ PREFIX_EVENT_END_DATE_TIME)) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT,
+ AddContactEventCommand.MESSAGE_USAGE));
+ }
+
+ argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME,
+ PREFIX_EVENT_END_DATE_TIME);
+ EventDescription description = ParserUtil.parseEventDescription(argMultimap
+ .getValue(PREFIX_EVENT_DESCRIPTION).get());
+ EventPeriod eventPeriod = ParserUtil.parseEventPeriod(argMultimap.getValue(PREFIX_EVENT_START_DATE_TIME).get(),
+ argMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).get());
+
+ return new AddContactEventCommand(index, new Event(description, eventPeriod));
+ }
+
+ /**
+ * Checks if all the given prefix fields are non-empty.
+ *
+ * @param argumentMultimap argumentMultimap managing arguments for this command.
+ * @param prefixes prefixes to be tested.
+ * @return true if all fields are non-empty, false if any field contains empty value.
+ */
+ 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..766aa3bef3d
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/AddEventCommandParser.java
@@ -0,0 +1,51 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME;
+
+import java.util.stream.Stream;
+
+import seedu.address.logic.commands.AddEventCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.event.Event;
+import seedu.address.model.event.EventDescription;
+import seedu.address.model.event.EventPeriod;
+
+/**
+ * Parses input arguments and creates a new AddEventCommand object.
+ */
+public class AddEventCommandParser implements Parser {
+ @Override
+ public AddEventCommand parse(String args) throws ParseException {
+ ArgumentMultimap argMultiMap =
+ ArgumentTokenizer.tokenize(args, PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME,
+ PREFIX_EVENT_END_DATE_TIME);
+
+ if (!arePrefixesPresent(argMultiMap, PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME,
+ PREFIX_EVENT_END_DATE_TIME) || !argMultiMap.getPreamble().isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddEventCommand.MESSAGE_USAGE));
+ }
+
+ argMultiMap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME,
+ PREFIX_EVENT_END_DATE_TIME);
+ EventDescription description = ParserUtil.parseEventDescription(argMultiMap
+ .getValue(PREFIX_EVENT_DESCRIPTION).get());
+ EventPeriod eventPeriod = ParserUtil.parseEventPeriod(argMultiMap.getValue(PREFIX_EVENT_START_DATE_TIME).get(),
+ argMultiMap.getValue(PREFIX_EVENT_END_DATE_TIME).get());
+
+ return new AddEventCommand(new Event(description, eventPeriod));
+ }
+
+ /**
+ * Checks if all the given prefix fields are non-empty.
+ *
+ * @param argumentMultimap argumentMultimap managing arguments for this command.
+ * @param prefixes prefixes to be tested.
+ * @return true if all fields are non-empty, false if any field contains empty value.
+ */
+ 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/AddTaskCommandParser.java b/src/main/java/seedu/address/logic/parser/AddTaskCommandParser.java
new file mode 100644
index 00000000000..621d8f321a3
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/AddTaskCommandParser.java
@@ -0,0 +1,58 @@
+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.commands.AddTaskCommand.MESSAGE_INVALID_DESCRIPTION;
+import static seedu.address.logic.commands.AddTaskCommand.MESSAGE_USAGE;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_DESCRIPTION;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME;
+import static seedu.address.model.task.Deadline.MESSAGE_CONSTRAINTS;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeParseException;
+
+import seedu.address.logic.commands.AddTaskCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.task.Task;
+
+/**
+ * Parses input arguments and creates a new addTaskCommand object
+ */
+public class AddTaskCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the AddTaskCommand
+ * and returns an AddTaskCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public AddTaskCommand parse(String args) throws ParseException {
+ requireNonNull(args);
+
+
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args, PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_END_DATE_TIME);
+
+ argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_END_DATE_TIME);
+
+ if (argMultimap.getValue(PREFIX_EVENT_DESCRIPTION).isEmpty() || !argMultimap.getPreamble().isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTaskCommand.MESSAGE_USAGE));
+ }
+
+ String description = argMultimap.getValue(PREFIX_EVENT_DESCRIPTION).get();
+ if (description.equals("")) {
+ throw new ParseException(String.format(MESSAGE_INVALID_DESCRIPTION, MESSAGE_USAGE));
+ }
+ LocalDateTime deadline = null;
+
+ if (argMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).isPresent()) {
+ try {
+ deadline = ParserUtil.parseDateTime(argMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).get());
+ } catch (DateTimeParseException e) {
+ throw new ParseException(MESSAGE_CONSTRAINTS);
+ }
+ }
+
+ Task toAdd = new Task(description, deadline);
+ return new AddTaskCommand(toAdd);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/ClearEventsCommandParser.java b/src/main/java/seedu/address/logic/parser/ClearEventsCommandParser.java
new file mode 100644
index 00000000000..d790e38a2d6
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/ClearEventsCommandParser.java
@@ -0,0 +1,48 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_CONFIRMATION;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME;
+
+import seedu.address.logic.commands.ClearEventsCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.event.EventPeriod;
+
+/**
+ * Parses the input arguments and creates a new ClearEventsCommand object.
+ */
+public class ClearEventsCommandParser implements Parser {
+ private static final String CONFIRMATION = "CONFIRMED";
+ @Override
+ public ClearEventsCommand parse(String userInput) throws ParseException {
+ ArgumentMultimap argMultiMap =
+ ArgumentTokenizer.tokenize(userInput, PREFIX_EVENT_START_DATE_TIME, PREFIX_EVENT_END_DATE_TIME,
+ PREFIX_CONFIRMATION);
+
+ if (!isTimePresent(argMultiMap) || !argMultiMap.getPreamble().isEmpty()) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ClearEventsCommand.MESSAGE_USAGE));
+ }
+
+ argMultiMap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_START_DATE_TIME, PREFIX_EVENT_END_DATE_TIME,
+ PREFIX_CONFIRMATION);
+
+ boolean isConfirmed = isCommandConfirmed(argMultiMap);
+ EventPeriod clearPeriod = ParserUtil.parseEventPeriod(argMultiMap.getValue(PREFIX_EVENT_START_DATE_TIME).get(),
+ argMultiMap.getValue(PREFIX_EVENT_END_DATE_TIME).get());
+ return new ClearEventsCommand(clearPeriod, isConfirmed);
+ }
+
+ private static boolean isCommandConfirmed(ArgumentMultimap argumentMultimap) {
+ if (argumentMultimap.getValue(PREFIX_CONFIRMATION).isPresent()) {
+ return argumentMultimap.getValue(PREFIX_CONFIRMATION).get().equals(CONFIRMATION);
+ }
+ return false;
+ }
+
+ private static boolean isTimePresent(ArgumentMultimap argumentMultimap) {
+ boolean isStartTimePresent = argumentMultimap.getValue(PREFIX_EVENT_START_DATE_TIME).isPresent();
+ boolean isEndTimePresent = argumentMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).isPresent();
+ return isStartTimePresent && isEndTimePresent;
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java
index 75b1a9bf119..b2c3ed12b62 100644
--- a/src/main/java/seedu/address/logic/parser/CliSyntax.java
+++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java
@@ -12,4 +12,10 @@ public class CliSyntax {
public static final Prefix PREFIX_ADDRESS = new Prefix("a/");
public static final Prefix PREFIX_TAG = new Prefix("t/");
+ public static final Prefix PREFIX_EVENT_DESCRIPTION = new Prefix("d/");
+
+ public static final Prefix PREFIX_EVENT_START_DATE_TIME = new Prefix("ts/");
+
+ public static final Prefix PREFIX_EVENT_END_DATE_TIME = new Prefix("te/");
+ public static final Prefix PREFIX_CONFIRMATION = new Prefix("c/");
}
diff --git a/src/main/java/seedu/address/logic/parser/CompareCalendarByIndexCommandParser.java b/src/main/java/seedu/address/logic/parser/CompareCalendarByIndexCommandParser.java
new file mode 100644
index 00000000000..866b2a115ee
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/CompareCalendarByIndexCommandParser.java
@@ -0,0 +1,37 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.CompareCalendarByIndexCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.logic.parser.exceptions.RuntimeParseException;
+
+/**
+ * Parses input arguments and creates a new CompareCalendarCommand object.
+ */
+public class CompareCalendarByIndexCommandParser implements Parser {
+ private static final String splitRegex = "\\s+";
+ @Override
+ public CompareCalendarByIndexCommand parse(String userInput) throws ParseException {
+ try {
+ List indexList = new ArrayList();
+ String personIndexString = userInput.trim();
+ if (!personIndexString.isEmpty()) {
+ String[] personIndexArray = personIndexString.split(splitRegex);
+ indexList = Arrays.stream(personIndexArray)
+ .map(ParserUtil::parseIndexSafe)
+ .collect(Collectors.toList());
+ }
+ return new CompareCalendarByIndexCommand(indexList);
+ } catch (RuntimeParseException pe) {
+ throw new ParseException(String.format(
+ MESSAGE_INVALID_COMMAND_FORMAT, CompareCalendarByIndexCommand.MESSAGE_USAGE));
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/CompareCalendarByTagCommandParser.java b/src/main/java/seedu/address/logic/parser/CompareCalendarByTagCommandParser.java
new file mode 100644
index 00000000000..0188065b2e7
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/CompareCalendarByTagCommandParser.java
@@ -0,0 +1,35 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+import seedu.address.logic.commands.CompareCalendarByTagCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Parser for creating a CompareCalendarByTagCommand.
+ */
+public class CompareCalendarByTagCommandParser implements Parser {
+ private static final String splitRegex = "\\s+";
+ @Override
+ public CompareCalendarByTagCommand parse(String userInput) throws ParseException {
+ try {
+ String trimmedInput = userInput.trim();
+ List tagList = new ArrayList();
+ if (!trimmedInput.isEmpty()) {
+ String[] tagStringArray = trimmedInput.split(splitRegex);
+ Set tagSet = ParserUtil.parseTags(Arrays.asList(tagStringArray));
+ tagList.addAll(tagSet);
+ }
+ return new CompareCalendarByTagCommand(tagList);
+ } catch (ParseException pe) {
+ throw new ParseException(String.format(
+ MESSAGE_INVALID_COMMAND_FORMAT, CompareCalendarByTagCommand.MESSAGE_USAGE));
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java
index 3527fe76a3e..337cd47028e 100644
--- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java
+++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java
@@ -1,6 +1,8 @@
package seedu.address.logic.parser;
+import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW;
import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE;
import seedu.address.commons.core.index.Index;
import seedu.address.logic.commands.DeleteCommand;
@@ -21,9 +23,12 @@ public DeleteCommand parse(String args) throws ParseException {
Index index = ParserUtil.parseIndex(args);
return new DeleteCommand(index);
} catch (ParseException pe) {
- throw new ParseException(
- String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe);
+ if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) {
+ throw new ParseException(MESSAGE_INTEGER_OVERFLOW);
+ } else {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe);
+ }
}
}
-
}
diff --git a/src/main/java/seedu/address/logic/parser/DeleteContactEventCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteContactEventCommandParser.java
new file mode 100644
index 00000000000..0a39ffe0620
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/DeleteContactEventCommandParser.java
@@ -0,0 +1,69 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME;
+import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE;
+import static seedu.address.model.event.EventPeriod.DATE_TIME_STRING_FORMATTER;
+
+import java.time.LocalDateTime;
+import java.util.stream.Stream;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.DeleteContactEventCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new DeleteContactEventCommand object
+ */
+public class DeleteContactEventCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the DeleteContactEventCommand
+ * and returns an DeleteContactEventCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public DeleteContactEventCommand parse(String args) throws ParseException {
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args, PREFIX_EVENT_START_DATE_TIME);
+
+ Index index;
+ try {
+ index = ParserUtil.parseIndex(argMultimap.getPreamble());
+ } catch (ParseException pe) {
+ if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) {
+ throw new ParseException(MESSAGE_INTEGER_OVERFLOW);
+ } else {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteContactEventCommand.MESSAGE_USAGE), pe);
+ }
+ }
+
+ if (!arePrefixesPresent(argMultimap, PREFIX_EVENT_START_DATE_TIME)) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT,
+ DeleteContactEventCommand.MESSAGE_USAGE));
+ }
+
+ argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_START_DATE_TIME);
+ try {
+ LocalDateTime eventTime = LocalDateTime.parse(
+ argMultimap.getValue(PREFIX_EVENT_START_DATE_TIME).get(),
+ DATE_TIME_STRING_FORMATTER);
+ return new DeleteContactEventCommand(index, eventTime);
+ } catch (Exception pe) {
+ throw new ParseException(String.format(
+ MESSAGE_INVALID_COMMAND_FORMAT, DeleteContactEventCommand.MESSAGE_USAGE));
+ }
+ }
+
+ /**
+ * Checks if all the given prefix fields are non-empty.
+ *
+ * @param argumentMultimap argumentMultimap managing arguments for this command.
+ * @param prefixes prefixes to be tested.
+ * @return true if all fields are non-empty, false if any field contains empty value.
+ */
+ 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/DeleteEventCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteEventCommandParser.java
new file mode 100644
index 00000000000..95bd2d7222e
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/DeleteEventCommandParser.java
@@ -0,0 +1,26 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.ParserUtil.DATE_TIME_STRING_FORMATTER;
+
+import java.time.LocalDateTime;
+
+import seedu.address.logic.commands.DeleteEventCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new DeleteEventCommand object.
+ */
+public class DeleteEventCommandParser implements Parser {
+ @Override
+ public DeleteEventCommand parse(String userInput) throws ParseException {
+ try {
+ String eventString = userInput.trim();
+ LocalDateTime eventTime = LocalDateTime.parse(eventString, DATE_TIME_STRING_FORMATTER);
+ return new DeleteEventCommand(eventTime);
+ } catch (Exception pe) {
+ throw new ParseException(String.format(
+ MESSAGE_INVALID_COMMAND_FORMAT, DeleteEventCommand.MESSAGE_USAGE));
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/DeleteTaskCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteTaskCommandParser.java
new file mode 100644
index 00000000000..3c45656ada3
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/DeleteTaskCommandParser.java
@@ -0,0 +1,34 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.DeleteTaskCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new DeleteTaskCommand object
+ */
+public class DeleteTaskCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the DeleteTaskCommand
+ * and returns a DeleteTaskCommand object for execution.
+ * @throws ParseException if the use input does not conform to the expected format.
+ */
+ public DeleteTaskCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new DeleteTaskCommand(index);
+ } catch (ParseException pe) {
+ if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) {
+ throw new ParseException(MESSAGE_INTEGER_OVERFLOW);
+ } else {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteTaskCommand.MESSAGE_USAGE), pe);
+ }
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java
index 46b3309a78b..2e803662c3e 100644
--- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java
+++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java
@@ -1,23 +1,20 @@
package seedu.address.logic.parser;
import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW;
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.Collection;
-import java.util.Collections;
-import java.util.Optional;
-import java.util.Set;
+import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE;
+import static seedu.address.logic.parser.ParserUtil.parseTagsForUse;
import seedu.address.commons.core.index.Index;
import seedu.address.logic.commands.EditCommand;
import seedu.address.logic.commands.EditCommand.EditPersonDescriptor;
import seedu.address.logic.parser.exceptions.ParseException;
-import seedu.address.model.tag.Tag;
/**
* Parses input arguments and creates a new EditCommand object
@@ -39,7 +36,12 @@ public EditCommand parse(String args) throws ParseException {
try {
index = ParserUtil.parseIndex(argMultimap.getPreamble());
} catch (ParseException pe) {
- throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe);
+ if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) {
+ throw new ParseException(MESSAGE_INTEGER_OVERFLOW);
+ } else {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe);
+ }
}
argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS);
@@ -58,7 +60,7 @@ 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);
+ parseTagsForUse(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags);
if (!editPersonDescriptor.isAnyFieldEdited()) {
throw new ParseException(EditCommand.MESSAGE_NOT_EDITED);
@@ -67,19 +69,4 @@ public EditCommand parse(String args) throws ParseException {
return new EditCommand(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.
- */
- private Optional> parseTagsForEdit(Collection tags) throws ParseException {
- assert tags != null;
-
- if (tags.isEmpty()) {
- return Optional.empty();
- }
- Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags;
- return Optional.of(ParserUtil.parseTags(tagSet));
- }
-
}
diff --git a/src/main/java/seedu/address/logic/parser/EditContactEventCommandParser.java b/src/main/java/seedu/address/logic/parser/EditContactEventCommandParser.java
new file mode 100644
index 00000000000..db59d7cab60
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/EditContactEventCommandParser.java
@@ -0,0 +1,68 @@
+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_EVENT_DESCRIPTION;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_END_DATE_TIME;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_START_DATE_TIME;
+
+import java.util.ArrayList;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.EditContactEventCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new EditContactEventCommand object
+ */
+public class EditContactEventCommandParser implements Parser {
+ /**
+ * Parses the given {@code String} of arguments in the context of the EditContactEventCommand
+ * and returns an EditContactEventCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public EditContactEventCommand parse(String args) throws ParseException {
+ requireNonNull(args);
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args,
+ PREFIX_EVENT_DESCRIPTION, PREFIX_EVENT_START_DATE_TIME, PREFIX_EVENT_END_DATE_TIME);
+
+ String preamble;
+ ArrayList indexArrayList;
+
+ try {
+ preamble = argMultimap.getPreamble();
+ indexArrayList = ParserUtil.parseDualIndexes(preamble);
+ } catch (ParseException pe) {
+ throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT,
+ EditContactEventCommand.MESSAGE_USAGE), pe);
+ }
+
+ argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EVENT_DESCRIPTION,
+ PREFIX_EVENT_START_DATE_TIME, PREFIX_EVENT_END_DATE_TIME);
+
+ EditContactEventCommand.EditEventDescriptor editEventDescriptor =
+ new EditContactEventCommand.EditEventDescriptor();
+
+ if (argMultimap.getValue(PREFIX_EVENT_DESCRIPTION).isPresent()) {
+ editEventDescriptor.setEventDescription(ParserUtil.parseEventDescription(argMultimap
+ .getValue(PREFIX_EVENT_DESCRIPTION).get()));
+ }
+ boolean eventStartDateTimePresent = argMultimap.getValue(PREFIX_EVENT_START_DATE_TIME).isPresent();
+ boolean eventEndDateTimePresent = argMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).isPresent();
+ if (eventStartDateTimePresent && eventEndDateTimePresent) {
+ editEventDescriptor.setEventPeriod(
+ ParserUtil.parseEventPeriod(argMultimap.getValue(PREFIX_EVENT_START_DATE_TIME).get(),
+ argMultimap.getValue(PREFIX_EVENT_END_DATE_TIME).get()));
+ }
+ if ((eventStartDateTimePresent && !eventEndDateTimePresent)
+ || (!eventStartDateTimePresent && eventEndDateTimePresent)) {
+ throw new ParseException(EditContactEventCommand.MESSAGE_WRONG_TIME);
+ }
+ if (!editEventDescriptor.isAnyFieldEdited()) {
+ throw new ParseException(EditContactEventCommand.MESSAGE_NOT_EDITED);
+ }
+
+ return new EditContactEventCommand(indexArrayList, editEventDescriptor);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/FilterCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java
new file mode 100644
index 00000000000..f96342df51c
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java
@@ -0,0 +1,54 @@
+package seedu.address.logic.parser;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.commands.FilterCommand.CONTACTS_NOT_FILTERED;
+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.logic.parser.ParserUtil.parseTagsForUse;
+
+import seedu.address.logic.commands.FilterCommand;
+import seedu.address.logic.commands.FilterCommand.PersonFilter;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new FilterCommand object
+ */
+public class FilterCommandParser 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 FilterCommand parse(String args) throws ParseException {
+ requireNonNull(args);
+ ArgumentMultimap argMultimap =
+ ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG);
+
+ argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS);
+
+ PersonFilter personFilter = new PersonFilter();
+
+ if (argMultimap.getValue(PREFIX_NAME).isPresent()) {
+ personFilter.setName(argMultimap.getValue(PREFIX_NAME).get());
+ }
+ if (argMultimap.getValue(PREFIX_PHONE).isPresent()) {
+ personFilter.setPhone(argMultimap.getValue(PREFIX_PHONE).get());
+ }
+ if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) {
+ personFilter.setEmail(argMultimap.getValue(PREFIX_EMAIL).get());
+ }
+ if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) {
+ personFilter.setAddress(argMultimap.getValue(PREFIX_ADDRESS).get());
+ }
+ parseTagsForUse(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(personFilter::setTags);
+
+ if (!personFilter.isAnyFieldEdited()) {
+ throw new ParseException(CONTACTS_NOT_FILTERED);
+ }
+
+ return new FilterCommand(personFilter);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java
index b117acb9c55..cf2763d0c57 100644
--- a/src/main/java/seedu/address/logic/parser/ParserUtil.java
+++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java
@@ -1,14 +1,25 @@
package seedu.address.logic.parser;
import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.Optional;
import java.util.Set;
import seedu.address.commons.core.index.Index;
import seedu.address.commons.util.StringUtil;
import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.logic.parser.exceptions.RuntimeParseException;
+import seedu.address.model.event.EventDescription;
+import seedu.address.model.event.EventPeriod;
+import seedu.address.model.event.exceptions.InvalidEventPeriodException;
import seedu.address.model.person.Address;
import seedu.address.model.person.Email;
import seedu.address.model.person.Name;
@@ -19,9 +30,13 @@
* Contains utility methods used for parsing strings in the various *Parser classes.
*/
public class ParserUtil {
-
+ public static final String INVALID_INTEGER = "0";
public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer.";
-
+ public static final String MESSAGE_INDEX_TOO_LARGE = "Index given is more than maximum integer of 2147483647.";
+ public static final String NUMBER_INDEX_INVALID_INDEX = "Number of arguments provided is invalid.";
+ public static final DateTimeFormatter DATE_TIME_STRING_FORMATTER = DateTimeFormatter.ofPattern(
+ "yyyy-MM-dd HH:mm");
+ private static final String SPLIT_SPACE_DELIMITER = " ";
/**
* Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be
* trimmed.
@@ -29,12 +44,56 @@ public class ParserUtil {
*/
public static Index parseIndex(String oneBasedIndex) throws ParseException {
String trimmedIndex = oneBasedIndex.trim();
+ String allNumbersRegex = "\\d+";
if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) {
- throw new ParseException(MESSAGE_INVALID_INDEX);
+ throw trimmedIndex.matches(allNumbersRegex) && !trimmedIndex.equals(INVALID_INTEGER)
+ ? new ParseException(MESSAGE_INDEX_TOO_LARGE)
+ : new ParseException(MESSAGE_INVALID_INDEX);
}
return Index.fromOneBased(Integer.parseInt(trimmedIndex));
}
+ /**
+ * Parses given string and returns an array consisting of 2 Indexes. Leading and trailing whitespaces will be
+ * trimmed.
+ * @throws ParseException if the specified index is invalid (not non-zero unsigned integer).
+ */
+ public static ArrayList parseDualIndexes(String oneBasedIndex) throws ParseException {
+ final int maxParseIndexes = 2;
+
+ ArrayList arrayList = new ArrayList<>();
+ String[] indexArray = oneBasedIndex.split(SPLIT_SPACE_DELIMITER);
+
+ if (indexArray.length != maxParseIndexes) {
+ throw new ParseException(NUMBER_INDEX_INVALID_INDEX);
+ }
+
+ String trimmedIndex1 = indexArray[0].trim();
+ String trimmedIndex2 = indexArray[1].trim();
+ if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex1)
+ || !StringUtil.isNonZeroUnsignedInteger(trimmedIndex2)) {
+ throw new ParseException(MESSAGE_INVALID_INDEX);
+ }
+ arrayList.add(Index.fromOneBased(Integer.parseInt(trimmedIndex1)));
+ arrayList.add(Index.fromOneBased(Integer.parseInt(trimmedIndex2)));
+ return arrayList;
+ }
+
+ /**
+ * Wrapped version of parseIndex that throws a RuntimeException instead.
+ *
+ * @param oneBasedIndex index integer to be converted into Index object.
+ * @return Index object that is parsed from the input index integer.
+ * @throws RuntimeException if the specified index is invalid (not non-zero unsigned integer).
+ */
+ public static Index parseIndexSafe(String oneBasedIndex) throws RuntimeParseException {
+ try {
+ return parseIndex(oneBasedIndex);
+ } catch (ParseException pe) {
+ throw new RuntimeParseException();
+ }
+ }
+
/**
* Parses a {@code String name} into a {@code Name}.
* Leading and trailing whitespaces will be trimmed.
@@ -121,4 +180,91 @@ public static Set parseTags(Collection tags) throws ParseException
}
return tagSet;
}
+
+ /**
+ * 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.
+ */
+ public static Optional> parseTagsForUse(Collection tags) throws ParseException {
+ assert tags != null;
+
+ if (tags.isEmpty()) {
+ return Optional.empty();
+ }
+ Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags;
+ return Optional.of(ParserUtil.parseTags(tagSet));
+ }
+
+ /**
+ * Parses the event description String into a EventDescription object.
+ *
+ * @param description description String.
+ * @return EventDescription object with the given description String.
+ * @throws ParseException if the description is empty.
+ */
+ public static EventDescription parseEventDescription(String description) throws ParseException {
+ requireNonNull(description);
+ String trimmedDescription = description.trim();
+ if (!EventDescription.isValid(description)) {
+ throw new ParseException(EventDescription.MESSAGE_CONSTRAINTS);
+ }
+ return new EventDescription(trimmedDescription);
+ }
+
+ /**
+ * Parses given start date string and end date string into an EventPeriod object.
+ *
+ * @param startDate start date string in 'yyyy-MM-dd HH:mm' format.
+ * @param endDate end date string in 'yyyy-MM-dd HH:mm' format.
+ * @return EventPeriod object describing the time period between startDate and endDate.
+ * @throws ParseException if the startDate or endDate strings are in improper format.
+ */
+ public static EventPeriod parseEventPeriod(String startDate, String endDate) throws ParseException {
+ requireAllNonNull(startDate, endDate);
+ String trimmedStartDate = startDate.trim();
+ String trimmedEndDate = endDate.trim();
+ try {
+ EventPeriod.isValidPeriod(trimmedStartDate, trimmedEndDate);
+ } catch (DateTimeParseException dateTimeParseException) {
+ throw new ParseException(EventPeriod.MESSAGE_CONSTRAINTS);
+ } catch (InvalidEventPeriodException invalidEventPeriodException) {
+ throw new ParseException(EventPeriod.PERIOD_INVALID);
+ }
+ return new EventPeriod(trimmedStartDate, trimmedEndDate);
+ }
+
+ /**
+ * Parses given DateTime String into a LocalDateTime object.
+ *
+ * @param dateTime a date time string in 'yyyy-MM-dd HH:mm' format.
+ * @return LocalDateTime object describing the time and date.
+ * @throws ParseException if the string is not in the stated format.
+ */
+ public static LocalDateTime parseDateTime(String dateTime) throws ParseException {
+ requireNonNull(dateTime);
+ String trimmedDateTime = dateTime.trim();
+ return LocalDateTime.parse(trimmedDateTime, DATE_TIME_STRING_FORMATTER);
+ }
+
+ /**
+ * Parses given DateTime String into a LocalDateTime object.
+ *
+ * @param dateTime a date time string in 'yyyy-MM-dd HH:mm' format.
+ * @return LocalDateTime object describing the time and date.
+ */
+ public static LocalDateTime parseDateTimeNonNull(String dateTime) {
+ String trimmedDateTime = dateTime.trim();
+ return LocalDateTime.parse(trimmedDateTime, DATE_TIME_STRING_FORMATTER);
+ }
+
+ /**
+ * Parses given LocalDateTime into a String object.
+ *
+ * @param dateTime a LocalDateTime object.
+ * @return String object describing the time and date in the format of 'yyyy-MM-dd HH:mm'.
+ */
+ public static String parseDateTimeToString(LocalDateTime dateTime) {
+ return dateTime.format(DATE_TIME_STRING_FORMATTER);
+ }
}
diff --git a/src/main/java/seedu/address/logic/parser/SortCommandParser.java b/src/main/java/seedu/address/logic/parser/SortCommandParser.java
new file mode 100644
index 00000000000..73ed9bc1158
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/SortCommandParser.java
@@ -0,0 +1,103 @@
+package seedu.address.logic.parser;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import seedu.address.logic.commands.SortCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.person.comparer.AddressComparator;
+import seedu.address.model.person.comparer.EmailComparator;
+import seedu.address.model.person.comparer.NameComparator;
+import seedu.address.model.person.comparer.PhoneComparator;
+import seedu.address.model.person.comparer.SortComparator;
+
+/**
+ * Parses input arguments and creates a new SortCommand object
+ */
+public class SortCommandParser implements Parser {
+
+ public static final String PARSE_EXCEPTION_MESSAGE = "Invalid syntax for sort. Please use the "
+ + "following syntax: sort {/byattribute} {/reverse (optional)}. Example: sort /byname /reverse."
+ + " Possible attributes: /byname, /byemail, /byaddress, /byphone ";
+ private static final Prefix PREFIX_DELIMITER = new Prefix("/");
+ private static final String SORT_BY_ADDRESS_KEYWORD = "byaddress";
+ private static final String SORT_BY_EMAIL_KEYWORD = "byemail";
+ private static final String SORT_BY_NAME_KEYWORD = "byname";
+ private static final String SORT_BY_PHONE_KEYWORD = "byphone";
+ private static final String REVERSE_KEYWORD = "reverse";
+ private static final int MAX_SECONDARY_SORT = 0;
+ private static final int MAX_SORTS = MAX_SECONDARY_SORT + 1;
+ private static final int MAX_SORT_ARGS = 2 + MAX_SECONDARY_SORT * 2;
+ /**
+ * Parses the given {@code String} of arguments in the context of the SortCommand
+ * and returns an SortCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format
+ */
+ public SortCommand parse(String args) throws ParseException {
+ String trimmedArgs = args.trim();
+ if (trimmedArgs.isEmpty()) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE));
+ }
+ requireNonNull(args);
+ ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_DELIMITER);
+ argMultimap.verifyNoDuplicatePrefixesFor();
+
+ List sortValues = argMultimap.getAllValues(PREFIX_DELIMITER);
+
+ ArrayList sortComparatorList = new ArrayList<>();
+ boolean isEmptyArray = sortValues.isEmpty();
+ boolean isTooManyArgs = sortValues.size() > MAX_SORT_ARGS;
+ if (isEmptyArray || isTooManyArgs) {
+ throw new ParseException(PARSE_EXCEPTION_MESSAGE);
+ }
+
+ for (String command: sortValues) {
+ String commandLower = command.toLowerCase();
+ SortComparator sortComparator;
+
+ if (!commandLower.equals(REVERSE_KEYWORD) && sortComparatorList.size() > MAX_SECONDARY_SORT) {
+ throw new ParseException(PARSE_EXCEPTION_MESSAGE);
+ }
+
+ switch (commandLower) {
+ case SORT_BY_ADDRESS_KEYWORD:
+ sortComparator = new AddressComparator(true, false, 1);
+ sortComparatorList.add(sortComparator);
+ break;
+
+ case SORT_BY_EMAIL_KEYWORD:
+ sortComparator = new EmailComparator(true, false, 1);
+ sortComparatorList.add(sortComparator);
+ break;
+
+ case SORT_BY_NAME_KEYWORD:
+ sortComparator = new NameComparator(true, false, 1);
+ sortComparatorList.add(sortComparator);
+ break;
+
+ case SORT_BY_PHONE_KEYWORD:
+ sortComparator = new PhoneComparator(true, false, 1);
+ sortComparatorList.add(sortComparator);
+ break;
+
+ case REVERSE_KEYWORD:
+ if (sortComparatorList.isEmpty()) {
+ throw new ParseException(PARSE_EXCEPTION_MESSAGE);
+ }
+ int lastIdx = sortComparatorList.size() - 1;
+ sortComparatorList.get(lastIdx).setIsReverse(true);
+ break;
+
+ default:
+ throw new ParseException(PARSE_EXCEPTION_MESSAGE);
+ }
+ }
+
+ return new SortCommand(sortComparatorList);
+ }
+
+}
diff --git a/src/main/java/seedu/address/logic/parser/SortTasksCommandParser.java b/src/main/java/seedu/address/logic/parser/SortTasksCommandParser.java
new file mode 100644
index 00000000000..d0b188f53a3
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/SortTasksCommandParser.java
@@ -0,0 +1,26 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+
+import seedu.address.logic.commands.SortTasksCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new SortTasksCommand object.
+ */
+public class SortTasksCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the SortTasksCommand
+ * and returns a SortTasksCommand object for execution
+ * @throws ParseException if no parameters are provided.
+ */
+ public SortTasksCommand parse(String args) throws ParseException {
+ String trimmedArgs = args.trim();
+ if (trimmedArgs.isEmpty()) {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortTasksCommand.MESSAGE_USAGE));
+ }
+ return new SortTasksCommand(trimmedArgs);
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/UniMateParser.java
similarity index 52%
rename from src/main/java/seedu/address/logic/parser/AddressBookParser.java
rename to src/main/java/seedu/address/logic/parser/UniMateParser.java
index 3149ee07e0b..befac266cf8 100644
--- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java
+++ b/src/main/java/seedu/address/logic/parser/UniMateParser.java
@@ -9,26 +9,41 @@
import seedu.address.commons.core.LogsCenter;
import seedu.address.logic.commands.AddCommand;
+import seedu.address.logic.commands.AddContactEventCommand;
+import seedu.address.logic.commands.AddEventCommand;
+import seedu.address.logic.commands.AddTaskCommand;
import seedu.address.logic.commands.ClearCommand;
+import seedu.address.logic.commands.ClearEventsCommand;
import seedu.address.logic.commands.Command;
+import seedu.address.logic.commands.CompareCalendarByIndexCommand;
+import seedu.address.logic.commands.CompareCalendarByTagCommand;
import seedu.address.logic.commands.DeleteCommand;
+import seedu.address.logic.commands.DeleteContactEventCommand;
+import seedu.address.logic.commands.DeleteEventCommand;
+import seedu.address.logic.commands.DeleteTaskCommand;
import seedu.address.logic.commands.EditCommand;
+import seedu.address.logic.commands.EditContactEventCommand;
import seedu.address.logic.commands.ExitCommand;
+import seedu.address.logic.commands.FilterCommand;
import seedu.address.logic.commands.FindCommand;
import seedu.address.logic.commands.HelpCommand;
import seedu.address.logic.commands.ListCommand;
+import seedu.address.logic.commands.SortCommand;
+import seedu.address.logic.commands.SortTasksCommand;
+import seedu.address.logic.commands.SwitchListCommand;
+import seedu.address.logic.commands.ViewContactEventsCommand;
import seedu.address.logic.parser.exceptions.ParseException;
/**
- * Parses user input.
+ * Parses user input for UniMate.
*/
-public class AddressBookParser {
+public class UniMateParser {
/**
* Used for initial separation of command word and args.
*/
private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)");
- private static final Logger logger = LogsCenter.getLogger(AddressBookParser.class);
+ private static final Logger logger = LogsCenter.getLogger(UniMateParser.class);
/**
* Parses user input into command for execution.
@@ -77,10 +92,54 @@ public Command parseCommand(String userInput) throws ParseException {
case HelpCommand.COMMAND_WORD:
return new HelpCommand();
+ case FilterCommand.COMMAND_WORD:
+ return new FilterCommandParser().parse(arguments);
+
+ case SortCommand.COMMAND_WORD:
+ return new SortCommandParser().parse(arguments);
+
+ case AddEventCommand.COMMAND_WORD:
+ return new AddEventCommandParser().parse(arguments);
+
+ case DeleteEventCommand.COMMAND_WORD:
+ return new DeleteEventCommandParser().parse(arguments);
+
+ case ClearEventsCommand.COMMAND_WORD:
+ return new ClearEventsCommandParser().parse(arguments);
+
+ case AddContactEventCommand.COMMAND_WORD:
+ return new AddContactEventCommandParser().parse(arguments);
+
+ case EditContactEventCommand.COMMAND_WORD:
+ return new EditContactEventCommandParser().parse(arguments);
+
+ case DeleteContactEventCommand.COMMAND_WORD:
+ return new DeleteContactEventCommandParser().parse(arguments);
+
+ case AddTaskCommand.COMMAND_WORD:
+ return new AddTaskCommandParser().parse(arguments);
+
+ case SwitchListCommand.COMMAND_WORD:
+ return new SwitchListCommand();
+
+ case SortTasksCommand.COMMAND_WORD:
+ return new SortTasksCommandParser().parse(arguments);
+
+ case DeleteTaskCommand.COMMAND_WORD:
+ return new DeleteTaskCommandParser().parse(arguments);
+
+ case ViewContactEventsCommand.COMMAND_WORD:
+ return new ViewContactEventsCommandParser().parse(arguments);
+
+ case CompareCalendarByIndexCommand.COMMAND_WORD:
+ return new CompareCalendarByIndexCommandParser().parse(arguments);
+
+ case CompareCalendarByTagCommand.COMMAND_WORD:
+ return new CompareCalendarByTagCommandParser().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/ViewContactEventsCommandParser.java b/src/main/java/seedu/address/logic/parser/ViewContactEventsCommandParser.java
new file mode 100644
index 00000000000..266a41a301f
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/ViewContactEventsCommandParser.java
@@ -0,0 +1,34 @@
+package seedu.address.logic.parser;
+
+import static seedu.address.logic.Messages.MESSAGE_INTEGER_OVERFLOW;
+import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.ParserUtil.MESSAGE_INDEX_TOO_LARGE;
+
+import seedu.address.commons.core.index.Index;
+import seedu.address.logic.commands.ViewContactEventsCommand;
+import seedu.address.logic.parser.exceptions.ParseException;
+
+/**
+ * Parses input arguments and creates a new ViewContactEventsCommand object.
+ */
+public class ViewContactEventsCommandParser implements Parser {
+
+ /**
+ * Parses the given {@code String} of arguments in the context of the ViewContactEventsCommand
+ * and returns a ViewContactEventsCommand object for execution.
+ * @throws ParseException if the user input does not conform the expected format.
+ */
+ public ViewContactEventsCommand parse(String args) throws ParseException {
+ try {
+ Index index = ParserUtil.parseIndex(args);
+ return new ViewContactEventsCommand(index);
+ } catch (ParseException pe) {
+ if (pe.getMessage().equals(MESSAGE_INDEX_TOO_LARGE)) {
+ throw new ParseException(MESSAGE_INTEGER_OVERFLOW);
+ } else {
+ throw new ParseException(
+ String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewContactEventsCommand.MESSAGE_USAGE), pe);
+ }
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/logic/parser/exceptions/RuntimeParseException.java b/src/main/java/seedu/address/logic/parser/exceptions/RuntimeParseException.java
new file mode 100644
index 00000000000..11f1159723d
--- /dev/null
+++ b/src/main/java/seedu/address/logic/parser/exceptions/RuntimeParseException.java
@@ -0,0 +1,7 @@
+package seedu.address.logic.parser.exceptions;
+
+/**
+ * Thrown when there is a ParseException caught at runtime.
+ */
+public class RuntimeParseException extends RuntimeException {
+}
diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java
index 73397161e84..40c029c2986 100644
--- a/src/main/java/seedu/address/model/AddressBook.java
+++ b/src/main/java/seedu/address/model/AddressBook.java
@@ -2,6 +2,7 @@
import static java.util.Objects.requireNonNull;
+import java.util.Comparator;
import java.util.List;
import javafx.collections.ObservableList;
@@ -108,6 +109,14 @@ public ObservableList getPersonList() {
return persons.asUnmodifiableObservableList();
}
+ /**
+ * Sort persons in Person List according to comparator provided.
+ * @param comparator
+ */
+ public void sortPersons(Comparator comparator) {
+ persons.sortPersons(comparator);
+ }
+
@Override
public boolean equals(Object other) {
if (other == this) {
diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java
index d54df471c1f..6d29af3274d 100644
--- a/src/main/java/seedu/address/model/Model.java
+++ b/src/main/java/seedu/address/model/Model.java
@@ -1,11 +1,20 @@
package seedu.address.model;
import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.util.Comparator;
+import java.util.List;
import java.util.function.Predicate;
import javafx.collections.ObservableList;
import seedu.address.commons.core.GuiSettings;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.event.Event;
+import seedu.address.model.event.EventPeriod;
+import seedu.address.model.event.exceptions.EventNotFoundException;
import seedu.address.model.person.Person;
+import seedu.address.model.task.Task;
+import seedu.address.model.task.TaskManager;
/**
* The API of the Model component.
@@ -52,6 +61,36 @@ public interface Model {
/** Returns the AddressBook */
ReadOnlyAddressBook getAddressBook();
+ /**
+ * Returns the user prefs' calendar file path.
+ */
+ Path getCalendarFilePath();
+
+ /**
+ * Sets the user prefs' calendar file path.
+ */
+ void setCalendarFilePath(Path calendarFilePath);
+
+ /**
+ * Replaces calendar data with the data in {@code calendar}.
+ */
+ void setCalendar(ReadOnlyCalendar calendar);
+
+ /**
+ * Returns the Calendar
+ */
+ ReadOnlyCalendar getCalendar();
+
+ /**
+ * Sets the user prefs' task manager file path.
+ */
+ void setTaskManagerFilePath(Path taskManagerFilePath);
+
+ /**
+ * Returns the user prefs' task manager file path.
+ */
+ Path getTaskManagerFilePath();
+
/**
* Returns true if a person with the same identity as {@code person} exists in the address book.
*/
@@ -76,12 +115,111 @@ public interface Model {
*/
void setPerson(Person target, Person editedPerson);
+ /**
+ * Check if calendar can add event
+ *
+ * @param event event to be added
+ * @return true if possible, false otherwise
+ */
+ boolean canAddEvent(Event event);
+
+ /**
+ * Adds an event into the calendar
+ *
+ * @param event event to be added
+ */
+ void addEvent(Event event);
+
+ /**
+ * Deletes an event at the specified time.
+ *
+ * @param dateTime The specified time.
+ * @throws EventNotFoundException if there is no event found at the specified time.
+ */
+ void deleteEventAt(LocalDateTime dateTime) throws EventNotFoundException;
+
+ /**
+ * Checks for an event at the specified time.
+ *
+ * @param dateTime the specified time.
+ * @return the Event
+ * @throws EventNotFoundException if there is no event at the specified time.
+ */
+ Event findEventAt(LocalDateTime dateTime) throws EventNotFoundException;
+
+ /**
+ * Looks for all events within a time range and returns them in a list.
+ *
+ * @param range an {@code EventPeriod} representing a time range.
+ * @return A list that contains all events within the time range.
+ */
+ List eventsInRange(EventPeriod range);
+
+ /**
+ * Looks for all events within a time range and deletes them.
+ *
+ * @param range an {@code EventPeriod} representing a time range.
+ */
+ void deleteEventsInRange(EventPeriod range);
+
+
+ /**
+ * Get the calendar resulting from the calendar comparison operation.
+ */
+ ReadOnlyCalendar getComparisonCalendar();
+
+ /**
+ * Set the event list after comparison of calendars.
+ *
+ * @param eventList event list generated from comparison of calendars.
+ */
+ void setComparisonCalendar(ReadOnlyCalendar eventList);
+
+ /** Returns a view of the event list */
+ ObservableList getEventList();
+
+ /** Returns a view of the task list */
+ ObservableList getTaskList();
+
+ /** Returns a view of the event list for the current week */
+ ObservableList getCurrentWeekEventList();
+
/** Returns an unmodifiable view of the filtered person list */
ObservableList getFilteredPersonList();
+ /**
+ * Adds a task to the TaskManager.
+ * @param task the task to be added.
+ */
+ void addTask(Task task);
+
+ /**
+ * Deletes a task from the TaskManager.
+ * @param index the displayed index of the task to delete
+ * @return the deleted task
+ */
+ Task deleteTask(int index);
+
+ /**
+ * Returns the TaskManager.
+ */
+ TaskManager getTaskManager();
+
+ /**
+ * Sorts the task list by the preset comparator type listed.
+ */
+ void sortTasksBy(String comparatorType);
+
/**
* Updates the filter of the filtered person list to filter by the given {@code predicate}.
* @throws NullPointerException if {@code predicate} is null.
*/
void updateFilteredPersonList(Predicate predicate);
+
+ /**
+ * Sorts the filtered person list by the given {@code comparator}.
+ * @throws NullPointerException if {@code comparator} is null.
+ */
+ void sortPersonList(Comparator personComparator);
}
+
diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java
index 57bc563fde6..9d2b9705dc6 100644
--- a/src/main/java/seedu/address/model/ModelManager.java
+++ b/src/main/java/seedu/address/model/ModelManager.java
@@ -4,6 +4,10 @@
import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.util.Comparator;
+import java.util.List;
+import java.util.NoSuchElementException;
import java.util.function.Predicate;
import java.util.logging.Logger;
@@ -11,7 +15,15 @@
import javafx.collections.transformation.FilteredList;
import seedu.address.commons.core.GuiSettings;
import seedu.address.commons.core.LogsCenter;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.calendar.UniMateCalendar;
+import seedu.address.model.event.Event;
+import seedu.address.model.event.EventPeriod;
+import seedu.address.model.event.exceptions.EventNotFoundException;
import seedu.address.model.person.Person;
+import seedu.address.model.task.ReadOnlyTaskManager;
+import seedu.address.model.task.Task;
+import seedu.address.model.task.TaskManager;
/**
* Represents the in-memory model of the address book data.
@@ -20,24 +32,31 @@ public class ModelManager implements Model {
private static final Logger logger = LogsCenter.getLogger(ModelManager.class);
private final AddressBook addressBook;
+ private final UniMateCalendar calendar;
+ private final TaskManager taskManager;
private final UserPrefs userPrefs;
private final FilteredList filteredPersons;
+ private ReadOnlyCalendar comparisonCalendar;
/**
- * Initializes a ModelManager with the given addressBook and userPrefs.
+ * Initializes a ModelManager with the given addressBook, calendar and userPrefs.
*/
- public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) {
+ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyCalendar calendar, ReadOnlyTaskManager taskManager,
+ ReadOnlyUserPrefs userPrefs) {
requireAllNonNull(addressBook, userPrefs);
logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs);
this.addressBook = new AddressBook(addressBook);
+ this.calendar = new UniMateCalendar(calendar);
+ this.taskManager = new TaskManager(taskManager);
this.userPrefs = new UserPrefs(userPrefs);
filteredPersons = new FilteredList<>(this.addressBook.getPersonList());
+ comparisonCalendar = new UniMateCalendar();
}
public ModelManager() {
- this(new AddressBook(), new UserPrefs());
+ this(new AddressBook(), new UniMateCalendar(), new TaskManager(), new UserPrefs());
}
//=========== UserPrefs ==================================================================================
@@ -75,6 +94,29 @@ public void setAddressBookFilePath(Path addressBookFilePath) {
userPrefs.setAddressBookFilePath(addressBookFilePath);
}
+ @Override
+ public Path getCalendarFilePath() {
+ return userPrefs.getCalendarFilePath();
+ }
+
+ @Override
+ public void setCalendarFilePath(Path calendarFilePath) {
+ requireNonNull(calendarFilePath);
+ userPrefs.setCalendarFilePath(calendarFilePath);
+ }
+
+ @Override
+ public Path getTaskManagerFilePath() {
+ return userPrefs.getTaskManagerFilePath();
+ }
+
+ @Override
+ public void setTaskManagerFilePath(Path taskManagerFilePath) {
+ requireNonNull(taskManagerFilePath);
+ userPrefs.setTaskManagerFilePath(taskManagerFilePath);
+ }
+
+
//=========== AddressBook ================================================================================
@Override
@@ -111,6 +153,118 @@ public void setPerson(Person target, Person editedPerson) {
addressBook.setPerson(target, editedPerson);
}
+ //=========== Sorted Person List Accessors ===============================================================
+ @Override
+ public void sortPersonList(Comparator personComparator) {
+ addressBook.sortPersons(personComparator);
+ }
+
+ //=========== Calendar ===================================================================================
+
+ @Override
+ public void setCalendar(ReadOnlyCalendar calendar) {
+ this.calendar.resetData(calendar);
+ }
+
+ @Override
+ public ReadOnlyCalendar getCalendar() {
+ return calendar;
+ }
+ @Override
+ public ObservableList getEventList() {
+ return calendar.getEventList();
+ }
+
+ @Override
+ public ObservableList getCurrentWeekEventList() {
+ return calendar.getCurrentWeekEventList();
+ }
+
+ @Override
+ public boolean canAddEvent(Event event) {
+ return calendar.canAddEvent(event);
+ }
+
+ @Override
+ public void addEvent(Event event) {
+ requireNonNull(event);
+
+ calendar.addEvent(event);
+ }
+
+ @Override
+ public void deleteEventAt(LocalDateTime dateTime) throws EventNotFoundException {
+ requireNonNull(dateTime);
+
+ calendar.deleteEventAt(dateTime);
+ }
+
+ @Override
+ public Event findEventAt(LocalDateTime dateTime) throws EventNotFoundException {
+ requireNonNull(dateTime);
+
+ try {
+ return calendar.findEventAt(dateTime).orElseThrow();
+ } catch (NoSuchElementException e) {
+ throw new EventNotFoundException();
+ }
+ }
+
+ @Override
+ public List eventsInRange(EventPeriod range) {
+ requireNonNull(range);
+
+ return calendar.getEventsInRange(range);
+ }
+
+ @Override
+ public void deleteEventsInRange(EventPeriod range) {
+ requireNonNull(range);
+
+ calendar.deleteEventsInRange(range);
+ }
+
+ @Override
+ public ReadOnlyCalendar getComparisonCalendar() {
+ return comparisonCalendar;
+ }
+
+ @Override
+ public void setComparisonCalendar(ReadOnlyCalendar comparisonCalendar) {
+ requireNonNull(comparisonCalendar);
+
+ this.comparisonCalendar = comparisonCalendar;
+ }
+
+ //=========== TaskManager ================================================================================
+
+ @Override
+ public TaskManager getTaskManager() {
+ return taskManager;
+ }
+
+ @Override
+ public void addTask(Task task) {
+ requireNonNull(task);
+
+ taskManager.addTask(task);
+ }
+
+ @Override
+ public Task deleteTask(int index) {
+ return taskManager.deleteTask(index);
+ }
+
+ @Override
+ public ObservableList getTaskList() {
+ return taskManager.getTaskList();
+ }
+
+ @Override
+ public void sortTasksBy(String comparatorType) {
+ taskManager.sortTasksBy(comparatorType);
+ }
+
//=========== Filtered Person List Accessors =============================================================
/**
@@ -141,8 +295,9 @@ public boolean equals(Object other) {
ModelManager otherModelManager = (ModelManager) other;
return addressBook.equals(otherModelManager.addressBook)
+ && calendar.equals(otherModelManager.calendar)
+ && taskManager.equals(otherModelManager.taskManager)
&& userPrefs.equals(otherModelManager.userPrefs)
&& filteredPersons.equals(otherModelManager.filteredPersons);
}
-
}
diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java
index 6be655fb4c7..47cdf900702 100644
--- a/src/main/java/seedu/address/model/UserPrefs.java
+++ b/src/main/java/seedu/address/model/UserPrefs.java
@@ -15,6 +15,8 @@ public class UserPrefs implements ReadOnlyUserPrefs {
private GuiSettings guiSettings = new GuiSettings();
private Path addressBookFilePath = Paths.get("data" , "addressbook.json");
+ private Path calendarFilePath = Paths.get("data", "calendar.json");
+ private Path taskManagerFilePath = Paths.get("data", "taskmanager.json");
/**
* Creates a {@code UserPrefs} with default values.
@@ -56,6 +58,24 @@ public void setAddressBookFilePath(Path addressBookFilePath) {
this.addressBookFilePath = addressBookFilePath;
}
+ public Path getCalendarFilePath() {
+ return calendarFilePath;
+ }
+
+ public void setCalendarFilePath(Path calendarFilePath) {
+ requireNonNull(calendarFilePath);
+ this.calendarFilePath = calendarFilePath;
+ }
+
+ public Path getTaskManagerFilePath() {
+ return taskManagerFilePath;
+ }
+
+ public void setTaskManagerFilePath(Path taskManagerFilePath) {
+ requireNonNull(taskManagerFilePath);
+ this.taskManagerFilePath = taskManagerFilePath;
+ }
+
@Override
public boolean equals(Object other) {
if (other == this) {
diff --git a/src/main/java/seedu/address/model/calendar/ReadOnlyCalendar.java b/src/main/java/seedu/address/model/calendar/ReadOnlyCalendar.java
new file mode 100644
index 00000000000..ecc0051d94c
--- /dev/null
+++ b/src/main/java/seedu/address/model/calendar/ReadOnlyCalendar.java
@@ -0,0 +1,48 @@
+package seedu.address.model.calendar;
+
+import java.time.LocalTime;
+import java.util.Optional;
+
+import javafx.collections.ObservableList;
+import seedu.address.model.event.Event;
+
+/**
+ * Unmodifiable view of the Calendar.
+ */
+public interface ReadOnlyCalendar {
+ /**
+ * Generates an unmodifiable view of the event list.
+ *
+ * @return unmodifiable view of event list.
+ */
+ ObservableList getEventList();
+
+ /**
+ * Generates an unmodifiable view of the event list for the current week.
+ *
+ * @return unmodifiable view of event list for the current week.
+ */
+ ObservableList getCurrentWeekEventList();
+
+ /**
+ * Retrieves the earliest starting time of any event for the current week in a LocalTime object.
+ *
+ * @return the earliest starting time of any event for the current week in a LocalTime object.
+ */
+ Optional getEarliestEventStartTimeInCurrentWeek();
+
+ /**
+ * Retrieves the latest ending time of any event for the current week in a LocalTime object.
+ *
+ * @return the latest ending time of any event for the current week in a LocalTime object.
+ */
+ Optional getLatestEventEndTimeInCurrentWeek();
+
+ /**
+ * Combine this calendar with another calendar, disregarding conflicts in events.
+ *
+ * @param other other calendar to be combined.
+ * @return new calendar with events from both calendars.
+ */
+ ReadOnlyCalendar getCombinedCalendar(ReadOnlyCalendar other);
+}
diff --git a/src/main/java/seedu/address/model/calendar/UniMateCalendar.java b/src/main/java/seedu/address/model/calendar/UniMateCalendar.java
new file mode 100644
index 00000000000..7a58d9b336b
--- /dev/null
+++ b/src/main/java/seedu/address/model/calendar/UniMateCalendar.java
@@ -0,0 +1,236 @@
+package seedu.address.model.calendar;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import seedu.address.model.event.AllDaysEventListManager;
+import seedu.address.model.event.Event;
+import seedu.address.model.event.EventPeriod;
+
+/**
+ * Represents a calendar that stores and manages events.
+ */
+public class UniMateCalendar implements ReadOnlyCalendar {
+ private static final int DAYS_IN_WEEK = 7;
+ private static final LocalDate DATE_OF_START_OF_CURRENT_WEEK = LocalDate.now().minusDays(
+ LocalDate.now().getDayOfWeek().getValue() - 1);
+ private static final LocalDate DATE_OF_END_OF_CURRENT_WEEK = LocalDate.now().plusDays(
+ DAYS_IN_WEEK - LocalDate.now().getDayOfWeek().getValue());
+ private final AllDaysEventListManager eventManager;
+ private final ObservableList internalList = FXCollections.observableArrayList();
+ private final ObservableList internalListForCurrentWeek = FXCollections.observableArrayList();
+
+ /**
+ * Constructs a Calendar object with an empty event tree.
+ */
+ public UniMateCalendar() {
+ this.eventManager = new AllDaysEventListManager();
+ }
+
+ /**
+ * Creates a Calendar using the Events in the {@code toBeCopied}
+ */
+ public UniMateCalendar(ReadOnlyCalendar toBeCopied) {
+ this();
+ resetData(toBeCopied);
+ }
+
+ //// overwrite operations
+
+ /**
+ * Replaces the contents of the Calendar with {@code events}.
+ * {@code events} must not contain duplicate events.
+ */
+ public void setEvents(List events) {
+ events.forEach(this::addEvent);
+ }
+
+ /**
+ * Resets the existing data of this {@code Calendar} with {@code newData}.
+ */
+ public void resetData(ReadOnlyCalendar newData) {
+ requireNonNull(newData);
+
+ setEvents(newData.getEventList());
+ }
+
+ /**
+ * Return the AllDaysEventListManager managing the events for this calendar.
+ *
+ * @return the AllDaysEventListManager managing the events.
+ */
+ public AllDaysEventListManager getEventManager() {
+ return this.eventManager;
+ }
+
+ /**
+ * Check if the event can be added to the calendar
+ *
+ * @param event event to be checked.
+ * @return true if it can be added, false otherwise.
+ */
+ public boolean canAddEvent(Event event) {
+ return eventManager.canAddEvent(event);
+ }
+
+ /**
+ * Adds an event to the calendar.
+ *
+ * @param event The event to be added.
+ */
+ public void addEvent(Event event) {
+ requireNonNull(event);
+ eventManager.addEvent(event);
+ internalList.setAll(eventManager.asUnmodifiableObservableList());
+ internalListForCurrentWeek.setAll(eventManager.asUnmodifiableObservableList(
+ DATE_OF_START_OF_CURRENT_WEEK, DATE_OF_END_OF_CURRENT_WEEK));
+ }
+
+ /**
+ * Force add the event into the event manager.
+ *
+ * @param event event to be added.
+ */
+ private void forceAddEvent(Event event) {
+ requireNonNull(event);
+ eventManager.forceAddEvent(event);
+ internalList.setAll(eventManager.asUnmodifiableObservableList());
+ internalListForCurrentWeek.setAll(eventManager.asUnmodifiableObservableList(
+ DATE_OF_START_OF_CURRENT_WEEK, DATE_OF_END_OF_CURRENT_WEEK));
+ }
+
+ /**
+ * Checks if there is an event at specified time and deletes it if there is.
+ * @param dateTime the specified time
+ */
+ public void deleteEventAt(LocalDateTime dateTime) {
+ requireNonNull(dateTime);
+ eventManager.deleteEventAt(dateTime);
+ internalList.setAll(eventManager.asUnmodifiableObservableList());
+ internalListForCurrentWeek.setAll(eventManager.asUnmodifiableObservableList(
+ DATE_OF_START_OF_CURRENT_WEEK, DATE_OF_END_OF_CURRENT_WEEK));
+ }
+
+ /**
+ * Looks for an event at specified time.
+ *
+ * @param dateTime the specified time.
+ * @return an optional containing the event if there is an event at the time, an empty optional otherwise.
+ */
+ public Optional findEventAt(LocalDateTime dateTime) {
+ requireNonNull(dateTime);
+ return eventManager.eventAt(dateTime);
+ }
+
+ /**
+ * Looks for all events within a specified time range and returns them in a list.
+ *
+ * @param range the specified time range represented by an {@code EventPeriod}
+ * @return a list containing all events within the range.
+ */
+ public List getEventsInRange(EventPeriod range) {
+ requireNonNull(range);
+ return eventManager.eventsInRange(range);
+ }
+
+ /**
+ * Looks for all events within a specified time range and deletes them.
+ *
+ * @param range the specified time range represented by an {@code EventPeriod}
+ */
+ public void deleteEventsInRange(EventPeriod range) {
+ requireNonNull(range);
+ for (Event event:getEventsInRange(range)) {
+ deleteEventAt(event.getStartDateTime());
+ }
+ }
+
+ /**
+ * Clears the calendar
+ */
+ public void clear() {
+ this.eventManager.clear();
+ }
+
+ /**
+ * Check if the calendar contains the event.
+ *
+ * @param event event to be checked.
+ * @return true if it is contained in the calendar, false otherwise.
+ */
+ public boolean contains(Event event) {
+ return this.eventManager.contains(event);
+ }
+
+ /**
+ * Checks if the calendar is empty.
+ *
+ * @return true if calendar is empty, false otherwise.
+ */
+ public boolean isEmpty() {
+ return this.eventManager.isEmpty();
+ }
+
+ /**
+ * Checks if there are any events at all in the calendar.
+ *
+ * @return true if there are events in the calendar, false otherwise.
+ */
+ public boolean hasEvents() {
+ if (!this.isEmpty()) {
+ return eventManager.hasEvents();
+ }
+ return false;
+ }
+
+ @Override
+ public ReadOnlyCalendar getCombinedCalendar(ReadOnlyCalendar other) {
+ requireNonNull(other);
+
+ UniMateCalendar combinedCalendar = new UniMateCalendar();
+ Stream.concat(internalList.stream(), other.getEventList().stream()).forEach(combinedCalendar::forceAddEvent);
+ return combinedCalendar;
+ }
+
+ @Override
+ public ObservableList getEventList() {
+ return internalList;
+ }
+
+ @Override
+ public ObservableList getCurrentWeekEventList() {
+ return internalListForCurrentWeek;
+ }
+
+ @Override
+ public Optional getEarliestEventStartTimeInCurrentWeek() {
+ return getCurrentWeekEventList().stream().min(Event::compareStartTime).map(Event::getStartTime);
+ }
+
+ @Override
+ public Optional getLatestEventEndTimeInCurrentWeek() {
+ return getCurrentWeekEventList().stream().max(Event::compareEndTime).map(Event::getEndTime);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof UniMateCalendar)) {
+ return false;
+ }
+
+ UniMateCalendar otherCalendar = (UniMateCalendar) other;
+ return this.eventManager.equals(otherCalendar.eventManager);
+ }
+}
diff --git a/src/main/java/seedu/address/model/event/AllDaysEventListManager.java b/src/main/java/seedu/address/model/event/AllDaysEventListManager.java
new file mode 100644
index 00000000000..04517578f4e
--- /dev/null
+++ b/src/main/java/seedu/address/model/event/AllDaysEventListManager.java
@@ -0,0 +1,232 @@
+package seedu.address.model.event;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.NavigableMap;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import seedu.address.model.event.exceptions.EventNotFoundException;
+
+/**
+ * Manages the SingleDayEventList objects for every day.
+ */
+public class AllDaysEventListManager {
+ private final NavigableMap dayToEventListMap;
+
+ /**
+ * Creates a AllDaysEventListManager to manage the daily events.
+ */
+ public AllDaysEventListManager() {
+ this.dayToEventListMap = new TreeMap();
+ }
+
+ /**
+ * Check if the event is conflicting with any events stored in the manager.
+ *
+ * @param event event to be added.
+ * @return true if the event can be added, false otherwise.
+ */
+ public boolean canAddEvent(Event event) {
+ List eventDays = event.getEventDays();
+ return eventDays.stream().filter(x -> dayToEventListMap.containsKey(x.toString()))
+ .allMatch(x -> dayToEventListMap.get(x.toString()).isEventAddValid(event));
+ }
+
+ /**
+ * Stores the event to the appropriate SingleDayEventList objects corresponding to the event's dates.
+ *
+ * @param event event to be added.
+ */
+ public void addEvent(Event event) {
+ List eventDays = event.getEventDays();
+ for (LocalDate date : eventDays) {
+ if (!(dayToEventListMap.containsKey(date.toString()))) {
+ dayToEventListMap.put(date.toString(), new SingleDayEventList(date));
+ }
+ dayToEventListMap.get(date.toString()).addEvent(event.boundEventByDate(date));
+ }
+ }
+
+ /**
+ * Forcibly add the event to the appropriate SingleDayEventList objects corresponding to the event's dates.
+ *
+ * @param event event to be added.
+ */
+ public void forceAddEvent(Event event) {
+ List eventDays = event.getEventDays();
+ for (LocalDate date : eventDays) {
+ if (!(dayToEventListMap.containsKey(date.toString()))) {
+ dayToEventListMap.put(date.toString(), new SingleDayEventList(date));
+ }
+ dayToEventListMap.get(date.toString()).forceAddEvent(event.boundEventByDate(date));
+ }
+ }
+
+ /**
+ * Clears the manager.
+ */
+ public void clear() {
+ this.dayToEventListMap.clear();
+ }
+
+ /**
+ * Check if the event is stored in this manager.
+ *
+ * @param event event to be checked.
+ * @return true if it is stored, false otherwise.
+ */
+ public boolean contains(Event event) {
+ for (LocalDate date : event.getEventDays()) {
+ if (!(this.dayToEventListMap.getOrDefault(date.toString(), new SingleDayEventList(date))
+ .containsEvent(event))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Check if this manager has an equivalent SingleDayEventList object stored in its dayToEventListMap.
+ *
+ * @param day SingleDayEventList object to be checked.
+ * @return true if there is a equivalent SingleDayEventList object stored in this manager, false otherwise.
+ */
+ private boolean contains(SingleDayEventList day) {
+ for (SingleDayEventList thisDay : this.dayToEventListMap.values()) {
+ if (thisDay.equals(day)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if there is an event at the specified time.
+ *
+ * @param dateTime the specified time.
+ * @return an optional containing the event at the specified time if there is an event, an empty optional otherwise.
+ */
+ public Optional eventAt(LocalDateTime dateTime) {
+ String key = dateTime.toLocalDate().toString();
+ if (dayToEventListMap.containsKey((key))) {
+ return dayToEventListMap.get(key).eventAtTime(dateTime);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Looks for an event at specified time and deletes it if found.
+ *
+ * @param dateTime the specified time.
+ * @throws EventNotFoundException if no event is found.
+ */
+ public void deleteEventAt(LocalDateTime dateTime) throws EventNotFoundException {
+ Optional optionalToDelete = eventAt(dateTime);
+ try {
+ Event toDelete = optionalToDelete.orElseThrow();
+ List days = toDelete.getEventDays();
+ days.stream().map(LocalDate::toString)
+ .forEach(x -> dayToEventListMap.get(x).remove(toDelete));
+ } catch (NoSuchElementException e) {
+ throw new EventNotFoundException();
+ }
+ }
+
+ /**
+ * Looks for events within a specified time and returns the list of events.
+ *
+ * @param range the {@code EventPeriod} describing the time range to check.
+ * @return A list of events or an empty list if no events are within the range.
+ */
+ public List eventsInRange(EventPeriod range) {
+ List days = range.getDates();
+ return days.stream().map(LocalDate::toString)
+ .flatMap(x -> dayToEventListMap.containsKey(x)
+ ? dayToEventListMap.get(x).eventsInRange(range).stream()
+ : Stream.empty())
+ .distinct()
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Checks if the manager is empty.
+ *
+ * @return true if the manager has no events stored, false otherwise.
+ */
+ public boolean isEmpty() {
+ return this.dayToEventListMap.isEmpty();
+ }
+
+ /**
+ * Returns the event list as an unmodifiable {@code ObservableList}.
+ */
+ public ObservableList asUnmodifiableObservableList() {
+ List list = dayToEventListMap.values().stream()
+ .flatMap(singleDayEventList -> singleDayEventList.getDayEventList().stream())
+ .map(Event::getParentEvent).distinct()
+ .collect(Collectors.toList());
+
+ return FXCollections.observableList(list);
+ }
+
+ /**
+ * Returns the event list storing events that occur between the start and end dates as an unmodifiable
+ * ObservableList.
+ *
+ * @param startDate start date.
+ * @param endDate end date.
+ * @return the event list storing events that occur between the start and end dates as an unmodifiable
+ * ObservableList.
+ */
+ public ObservableList asUnmodifiableObservableList(LocalDate startDate, LocalDate endDate) {
+ List list = dayToEventListMap.values().stream()
+ .flatMap(singleDayEventList -> singleDayEventList.getDayEventList().stream())
+ .filter(event -> event.occursBetweenDates(startDate, endDate))
+ .collect(Collectors.toList());
+
+ return FXCollections.observableList(list);
+ }
+
+ /**
+ * Checks if there are any events at all in the manager.
+ *
+ * @return true if there are any events in the manager, false otherwise.
+ */
+ public boolean hasEvents() {
+ if (!this.isEmpty()) {
+ return dayToEventListMap.values().stream().map(SingleDayEventList::isEmpty).anyMatch(x -> x.equals(false));
+ }
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof AllDaysEventListManager)) {
+ return false;
+ }
+
+ AllDaysEventListManager otherManager = (AllDaysEventListManager) other;
+
+ if (!(this.dayToEventListMap.size() == otherManager.dayToEventListMap.size())) {
+ return false;
+ }
+
+ for (SingleDayEventList day : this.dayToEventListMap.values()) {
+ if (!otherManager.contains(day)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
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..dcf923ffcd1
--- /dev/null
+++ b/src/main/java/seedu/address/model/event/Event.java
@@ -0,0 +1,314 @@
+package seedu.address.model.event;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+
+import java.time.DayOfWeek;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Represents an event in the calendar
+ */
+public class Event {
+ private EventDescription description;
+ private EventPeriod eventPeriod;
+ private Optional parentEvent;
+
+ /**
+ * Constructs an event with a given description occurring over the given time period.
+ *
+ * @param description The description of the event.
+ * @param eventPeriod The period of time when the event occurs.
+ */
+ public Event(EventDescription description, EventPeriod eventPeriod) {
+ requireAllNonNull(description, eventPeriod);
+
+ this.description = description;
+ this.eventPeriod = eventPeriod;
+ this.parentEvent = Optional.empty();
+ }
+
+ /**
+ * Constructs an event with a given description occurring over a given time period with a parent Event.
+ *
+ * @param description The description of the event.
+ * @param eventPeriod The period of the time when the event occurs.
+ * @param parent parent Event object this Event object was dervied from.
+ */
+ private Event(EventDescription description, EventPeriod eventPeriod, Event parent) {
+ requireAllNonNull(description, eventPeriod, parent);
+
+ this.description = description;
+ this.eventPeriod = eventPeriod;
+ this.parentEvent = Optional.of(parent);
+ }
+
+ /**
+ * Creates and returns a new Event object that is guaranteed to not conflict with any other event.
+ *
+ * @return A new non-conflicting Event object.
+ */
+ public static Event createNonConflictingEvent() {
+ return new Event(EventDescription.createUnusedDescription(), EventPeriod.createNonConflictingPeriod());
+ }
+
+ /**
+ * Gets the description of the event.
+ *
+ * @return The description of the event.
+ */
+ public EventDescription getDescription() {
+ return this.description;
+ }
+
+ /**
+ * Gets the time period during which the event occurs.
+ *
+ * @return The time period of the event.
+ */
+ public EventPeriod getEventPeriod() {
+ return this.eventPeriod;
+ }
+
+ /**
+ * Checks if this event overlaps with another event period
+ *
+ * @param period The other event period.
+ * @return True if there is an overlap, false otherwise.
+ */
+ public boolean isPeriodConflicting(EventPeriod period) {
+ requireNonNull(period);
+
+ return eventPeriod.isOverlapping(period);
+ }
+
+ /**
+ * Checks if this event conflicts with another event.
+ *
+ * @param other The other event to check for conflicts with.
+ * @return True if there is a conflict, false otherwise.
+ */
+ public boolean isConflicting(Event other) {
+ requireNonNull(other);
+
+ return isPeriodConflicting(other.eventPeriod);
+ }
+
+ /**
+ * Changes the description of the event.
+ *
+ * @param description The new description for the event.
+ */
+ public void changeDescription(EventDescription description) {
+ requireNonNull(description);
+
+ this.description = description;
+ }
+
+ /**
+ * Changes the time period during which the event occurs.
+ *
+ * @param eventPeriod The new time period for the event.
+ */
+ public void changeEventPeriod(EventPeriod eventPeriod) {
+ requireNonNull(eventPeriod);
+
+ this.eventPeriod = eventPeriod;
+ }
+
+ /**
+ * Get the dates the event spans, stored in a list.
+ *
+ * @return list of the dates the event spans.
+ */
+ public List getEventDays() {
+ return this.eventPeriod.getDates();
+ }
+
+ /**
+ * Checks if the {@code @LocalDateTime} is during the event.
+ *
+ * @param dateTime the specified {@code @LocalDateTime}.
+ * @return true if the time is during the event, false otherwise.
+ */
+ public boolean isDuring(LocalDateTime dateTime) {
+ requireNonNull(dateTime);
+
+ return this.eventPeriod.isWithin(dateTime);
+ }
+
+ /**
+ * Bounds the {@code @Event} such that the {@code @EventPeriod} of the formatted event happens within
+ * the specified {@code @LocalDate}.
+ *
+ * @param date the specified {@code @LocalDate}.
+ * @return new {@code @Event} with adjusted {@code @EventPeriod}.
+ */
+ public Event boundEventByDate(LocalDate date) {
+ requireNonNull(date);
+ if (hasParent()) {
+ return this;
+ }
+ return new Event(description, eventPeriod.boundPeriodByDate(date), this);
+ }
+
+ /**
+ * Get the parent Event object of this Event object.
+ *
+ * @return the parent Event object. If there is no parent, returns itself.
+ */
+ public Event getParentEvent() {
+ return parentEvent.orElse(this);
+ }
+
+ /**
+ * Checks if this Event object has a parent Event.
+ *
+ * @return true if it has a parent Event object, false otherwise.
+ */
+ public boolean hasParent() {
+ return parentEvent.isPresent();
+ }
+
+ public LocalDateTime getStartDateTime() {
+ if (hasParent()) {
+ return parentEvent.get().getStartDateTime();
+ }
+ return eventPeriod.getStart();
+ }
+
+ /**
+ * Checks if the Event's eventDuration spans a single day.
+ *
+ * @return true if it spans a single day, false otherwise.
+ */
+ private boolean isEventDurationSingleDay() {
+ return eventPeriod.isSingleDay();
+ }
+
+ /**
+ * Checks if parent Event's EventPeriod overlaps with the start and end dates(inclusive).
+ *
+ * @param start start date.
+ * @param end end date.
+ * @return true if the parent Event's EventPeriod overlaps with the start and end date.
+ */
+ public boolean occursBetweenDates(LocalDate start, LocalDate end) {
+ requireAllNonNull(start, end);
+ return eventPeriod.isOverlapping(start, end);
+ }
+
+ /**
+ * Compare the start time (independent of date) of this Event with another.
+ *
+ * @param other other Event.
+ * @return a negative integer if this Event has an earlier start time than the other, 0 if they have the same start
+ * time and a positive integer otherwise.
+ */
+ public int compareStartTime(Event other) {
+ requireNonNull(other);
+
+ return this.eventPeriod.compareStartTime(other.eventPeriod);
+ }
+
+ /**
+ * Compare the end time (independent of date) of this Event with another.
+ *
+ * @param other other Event.
+ * @return a negative integer if this Event has an earlier end time than the other, 0 if they have the same end time
+ * and a positive integer otherwise.
+ */
+ public int compareEndTime(Event other) {
+ requireNonNull(other);
+
+ return this.eventPeriod.compareEndTime(other.eventPeriod);
+ }
+
+ /**
+ * Get the start time of the event as a LocalTime object, omitting the date.
+ *
+ * @return the start time of the event as a LocalTime object, omitting the date.
+ */
+ public LocalTime getStartTime() {
+ return eventPeriod.getStartTime();
+ }
+
+ /**
+ * Get the end time of the event as a LocalTime object, omitting the date.
+ *
+ * @return the end time of the event as a LocalTime object, omitting the date.
+ */
+ public LocalTime getEndTime() {
+ return eventPeriod.getEndTime();
+ }
+
+ /**
+ * Get the duration of the Event, stored in a LocalTime object.
+ *
+ * @return LocalTime object of the duration of the Event.
+ */
+ public Duration getDurationOfEvent() {
+ return eventPeriod.getDuration();
+ }
+
+ /**
+ * Get the description of the Event in a String format.
+ *
+ * @return String of the description of the Event.
+ */
+ public String getDescriptionString() {
+ return description.getDescription();
+ }
+
+ /**
+ * Gets the time period during which the event occurs in a String format.
+ *
+ * @return String of the time period of the event.
+ */
+ public String getEventPeriodString() {
+ return eventPeriod.getFormattedPeriod();
+ }
+
+ /**
+ * Get the DayOfWeek of the Event. Event has to have a EventPeriod that spans a single day.
+ *
+ * @return DayOfWeek of the Event.
+ */
+ public DayOfWeek getDayOfWeek() {
+ assert(isEventDurationSingleDay());
+
+ return eventPeriod.getDayOfWeek();
+ }
+
+ /**
+ * Get the number of minutes elapsed from the input time to the start time of the Event.
+ *
+ * @param time input time.
+ * @return number of minutes elapsed from the input time to the start time of the Event.
+ */
+ public long getMinutesFromTimeToStartTime(LocalTime time) {
+ requireNonNull(time);
+ return eventPeriod.getMinutesFromTimeToStartTime(time);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof Event)) {
+ return false;
+ }
+
+ Event otherEvent = (Event) other;
+ return (otherEvent.description.equals(this.description)
+ && (otherEvent.eventPeriod.equals(this.eventPeriod)
+ || otherEvent.eventPeriod.isContinuous(this.eventPeriod)));
+ }
+}
diff --git a/src/main/java/seedu/address/model/event/EventDescription.java b/src/main/java/seedu/address/model/event/EventDescription.java
new file mode 100644
index 00000000000..c8ede35c8d3
--- /dev/null
+++ b/src/main/java/seedu/address/model/event/EventDescription.java
@@ -0,0 +1,74 @@
+package seedu.address.model.event;
+
+import static java.util.Objects.requireNonNull;
+
+import seedu.address.model.event.exceptions.InvalidDescriptionException;
+
+/**
+ * Represents the description of an event.
+ */
+public class EventDescription {
+ public static final String MESSAGE_CONSTRAINTS = "Description cannot be empty.";
+ private final String description;
+
+ /**
+ * Constructs an EventDescription object with the given description.
+ *
+ * @param description The description of the event.
+ */
+ public EventDescription(String description) {
+ requireNonNull(description);
+ if (description.isEmpty()) {
+ throw new InvalidDescriptionException();
+ }
+ this.description = description;
+ }
+
+ /**
+ * Creates and returns a new EventDescription object with a placeholder description.
+ *
+ * @return A new EventDescription object with a placeholder description.
+ */
+ public static EventDescription createUnusedDescription() {
+ return new EventDescription("THIS IS A PLACEHOLDER");
+ }
+
+ /**
+ * Checks if the given string is a valid description for creating a EventDescription object.
+ *
+ * @param description description String to be checked.
+ * @return true if description is non-empty, false if it is empty.
+ */
+ public static boolean isValid(String description) {
+ requireNonNull(description);
+ return !description.isEmpty();
+ }
+
+ /**
+ * Retrieve the underlying String of the description.
+ *
+ * @return String of the description.
+ */
+ public String getDescription() {
+ return this.description;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof EventDescription)) {
+ return false;
+ }
+
+ EventDescription otherDescription = (EventDescription) other;
+ return otherDescription.description.equals(this.description);
+ }
+
+ @Override
+ public String toString() {
+ return this.description;
+ }
+}
diff --git a/src/main/java/seedu/address/model/event/EventPeriod.java b/src/main/java/seedu/address/model/event/EventPeriod.java
new file mode 100644
index 00000000000..99fcf192902
--- /dev/null
+++ b/src/main/java/seedu/address/model/event/EventPeriod.java
@@ -0,0 +1,344 @@
+package seedu.address.model.event;
+
+import static java.time.temporal.ChronoUnit.MINUTES;
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+
+import java.time.DayOfWeek;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import seedu.address.model.event.exceptions.DateOutOfBoundsException;
+import seedu.address.model.event.exceptions.InvalidEventPeriodException;
+
+/**
+ * Represents a period in time when an event will occur.
+ */
+public class EventPeriod implements Comparable {
+ public static final String MESSAGE_CONSTRAINTS = "The start date and time and end date and time should "
+ + "be in the format 'yyyy-MM-dd HH:mm' where:\n"
+ + " -'yyyy' is the year.\n"
+ + " -'MM' is the month.\n"
+ + " -'dd' is the day.\n"
+ + " -'HH:mm' is the time in 24-hour format.";
+ public static final String PERIOD_INVALID = "The start date has to be before the end date.";
+ public static final DateTimeFormatter DATE_TIME_STRING_FORMATTER = DateTimeFormatter.ofPattern(
+ "yyyy-MM-dd HH:mm");
+ public static final LocalTime MAX_TIME_OF_DAY = LocalTime.MIDNIGHT.minusMinutes(1);
+
+ private final LocalDateTime start;
+ private final LocalDateTime end;
+
+ /**
+ * Constructs an EventPeriod object with the given start and end date/time strings.
+ *
+ * @param startString The string representation of the start date and time.
+ * @param endString The string representation of the end date and time.
+ */
+ public EventPeriod(String startString, String endString) {
+ this(LocalDateTime.parse(startString, DATE_TIME_STRING_FORMATTER),
+ LocalDateTime.parse(endString, DATE_TIME_STRING_FORMATTER));
+ }
+
+ /**
+ * Constructs an EventPeriod object with the given start and end date/time LocalDateTime.
+ *
+ * @param start The LocalDateTime representation of the start date and time.
+ * @param end The LocalDateTime representation of the end date and time.
+ */
+ private EventPeriod(LocalDateTime start, LocalDateTime end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ public LocalDateTime getStart() {
+ return this.start;
+ }
+
+ public LocalDateTime getEnd() {
+ return this.end;
+ }
+
+ /**
+ * Creates and returns a new EventPeriod object with minimum date/time values.
+ *
+ * @return A new EventPeriod object with minimum date/time values.
+ */
+ public static EventPeriod createNonConflictingPeriod() {
+ return new EventPeriod(LocalDateTime.MAX, LocalDateTime.MIN);
+ }
+
+ /**
+ * Checks if the given start and end date/time strings form a valid period.
+ *
+ * @param startString The string representation of the start date and time.
+ * @param endString The string representation of the end date and time.
+ * @return True if the period is valid, false otherwise.
+ */
+ public static boolean isValidPeriod(String startString, String endString) {
+ requireAllNonNull(startString, endString);
+
+ LocalDateTime startDateTime;
+ LocalDateTime endDateTime;
+ startDateTime = LocalDateTime.parse(startString, DATE_TIME_STRING_FORMATTER);
+ endDateTime = LocalDateTime.parse(endString, DATE_TIME_STRING_FORMATTER);
+
+ if (!startDateTime.isBefore(endDateTime)) {
+ throw new InvalidEventPeriodException(PERIOD_INVALID);
+ }
+ return true;
+ }
+
+ /**
+ * Checks if this EventPeriod overlaps with another EventPeriod.
+ *
+ * @param other The EventPeriod to check for overlap with.
+ * @return True if there is an overlap, false otherwise.
+ */
+ public boolean isOverlapping(EventPeriod other) {
+ requireNonNull(other);
+
+ return this.start.isBefore(other.end) && other.start.isBefore(this.end);
+ }
+
+ /**
+ * Checks if this EventPeriod overlaps with the given start date and end date.
+ *
+ * @param start start date.
+ * @param end end date.
+ * @return True if there is an overlap, false otherwise.
+ */
+ public boolean isOverlapping(LocalDate start, LocalDate end) {
+ requireAllNonNull(start, end);
+
+ boolean isThisEventStartingAfterOtherEventEnds = this.start.toLocalDate().isAfter(end);
+ boolean isThisEventEndedBeforeOtherEventStarts = this.end.toLocalDate().isBefore(start);
+ return !(isThisEventStartingAfterOtherEventEnds || isThisEventEndedBeforeOtherEventStarts);
+ }
+
+ /**
+ * Checks if a specified {@code @LocalDateTime} is within the {@code @EventPeriod}.
+ *
+ * @param dateTime the specified {@code @LocalDateTime}.
+ * @return True if it is within the period, false otherwise.
+ */
+ public boolean isWithin(LocalDateTime dateTime) {
+ requireNonNull(dateTime);
+ return (start.isEqual(dateTime) || start.isBefore(dateTime)) && end.isAfter(dateTime);
+ }
+
+ /**
+ * Compares this EventPeriod with another EventPeriod.
+ *
+ * @param other The EventPeriod to compare with.
+ * @return 1 if this EventPeriod is after the other, -1 if it's before, 0 if they are the same.
+ */
+ @Override
+ public int compareTo(EventPeriod other) {
+ requireNonNull(other);
+ if (this.start.isBefore(other.start)) {
+ return -1;
+ } else if (this.start.isEqual(other.start)) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+
+ /**
+ * Get the dates the eventPeriod spans, stored in an arrayList.
+ *
+ * @return list of the dates the eventPeriod spans.
+ */
+ public List getDates() {
+ LocalDate startDate = start.toLocalDate();
+ LocalDate endDate = end.toLocalDate();
+ return Stream.iterate(startDate, date -> !date.isAfter(endDate), date -> date.plusDays(1))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Get the string representation of the EventPeriod.
+ *
+ * @return string representation of the period the eventPeriod spans.
+ */
+ public String getFormattedPeriod() {
+ String startString = start.format(DATE_TIME_STRING_FORMATTER);
+ String endString = end.format(DATE_TIME_STRING_FORMATTER);
+ return startString + " - " + endString;
+ }
+
+ /**
+ * Get the string representation of the EventPeriod start DateTime.
+ *
+ * @return string representation of the start DateTime.
+ */
+ public String getFormattedStart() {
+ return start.format(DATE_TIME_STRING_FORMATTER);
+ }
+
+ /**
+ * Get the string representation of the EventPeriod end DateTime.
+ *
+ * @return string representation of the end DateTime.
+ */
+ public String getFormattedEnd() {
+ return end.format(DATE_TIME_STRING_FORMATTER);
+ }
+
+ /**
+ * Checks if another {@code @EventPeriod} occurs within a minute with this {@code @EventPeriod}.
+ *
+ * @param other the other {@code @EventPeriod}.
+ * @return true if they are apart by a single minute.
+ */
+ public boolean isContinuous(EventPeriod other) {
+ requireNonNull(other);
+
+ return this.start.plusMinutes(1) == other.end || other.start.plusMinutes(1) == this.end;
+ }
+
+ /**
+ * Bounds the {@code EventPeriod} such that the start and end lies within the specified {@code LocalDate}.
+ *
+ * @param date the specified {@code @LocalDate}.
+ * @return new {@code @EventPeriod} with adjusted start and end times.
+ */
+ public EventPeriod boundPeriodByDate(LocalDate date) {
+ requireNonNull(date);
+
+ boolean isDateOnStartDate = date.isEqual(start.toLocalDate());
+ boolean isDateOnEndDate = date.isEqual(end.toLocalDate());
+ boolean isDateAfterStartDate = date.isAfter(start.toLocalDate());
+ boolean isDateBeforeEndDate = date.isBefore(end.toLocalDate());
+
+
+ if (isDateOnStartDate && isDateOnEndDate) {
+ return this;
+ } else if (isDateOnStartDate) {
+ return new EventPeriod(start, LocalDateTime.of(date, MAX_TIME_OF_DAY));
+ } else if (isDateOnEndDate) {
+ return new EventPeriod(LocalDateTime.of(date, LocalTime.MIDNIGHT), end);
+ } else if (isDateAfterStartDate && isDateBeforeEndDate) {
+ return new EventPeriod(LocalDateTime.of(date, LocalTime.MIDNIGHT),
+ LocalDateTime.of(date, MAX_TIME_OF_DAY));
+ } else {
+ throw new DateOutOfBoundsException();
+ }
+ }
+
+ /**
+ * Compare the start time (independent of date) of this EventPeriod with another.
+ *
+ * @param other other EventPeriod object.
+ * @return a negative integer if this EventPeriod has an earlier start time than the other, 0 if both have the same
+ * start time and a positive integer otherwise.
+ */
+ public int compareStartTime(EventPeriod other) {
+ requireNonNull(other);
+
+ return this.start.toLocalTime().compareTo(other.start.toLocalTime());
+ }
+
+ /**
+ * Compare the end time (independent of date) of this EventPeriod with another.
+ *
+ * @param other other EventPeriod object.
+ * @return a negative integer if this EventPeriod has an earlier end time than the other, 0 if both have the same
+ * end time and a positive integer otherwise.
+ */
+ public int compareEndTime(EventPeriod other) {
+ requireNonNull(other);
+
+ return this.end.toLocalTime().compareTo(other.end.toLocalTime());
+ }
+
+ /**
+ * Get the start time as a LocalTime of the EventPeriod, omitting the date.
+ *
+ * @return the start time as a LocalTime of the EventPeriod.
+ */
+ public LocalTime getStartTime() {
+ return start.toLocalTime();
+ }
+
+ /**
+ * Get the end time as a LocalTime of the EventPeriod, omitting the date.
+ *
+ * @return the end time as a LocalTime of the EventPeriod.
+ */
+ public LocalTime getEndTime() {
+ return end.toLocalTime();
+ }
+
+ /**
+ * Get the duration of the EventPeriod stored in a LocalTime.
+ *
+ * @return duration of the EventPeriod stored in a LocalTime.
+ */
+ public Duration getDuration() {
+ Duration periodDuration = Duration.between(start, end);
+ if (end.toLocalTime().equals(MAX_TIME_OF_DAY)) {
+ periodDuration = periodDuration.plusMinutes(1);
+ }
+ return periodDuration;
+ }
+
+ /**
+ * Checks if the start and end LocalDateTime objects occur on the same date.
+ *
+ * @return true if they both occur on the same date, false otherwise.
+ */
+ public boolean isSingleDay() {
+ return start.toLocalDate().isEqual(end.toLocalDate());
+ }
+
+ /**
+ * Get the DayOfWeek of this EventPeriod. EventPeriod has to only span a single day.
+ *
+ * @return DayOfWeek of this EventPeriod.
+ */
+ public DayOfWeek getDayOfWeek() {
+ assert(isSingleDay());
+
+ return start.getDayOfWeek();
+ }
+
+ /**
+ * Get the number of minutes elapsed from the input time to the start time of this EventPeriod.
+ *
+ * @param time input time.
+ * @return minutes elapsed from the input time to the start time of the EventPeriod.
+ */
+ public long getMinutesFromTimeToStartTime(LocalTime time) {
+ return MINUTES.between(time, getStartTime());
+ }
+ @Override
+ public boolean equals(Object other) {
+ requireNonNull(other);
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof EventPeriod)) {
+ return false;
+ }
+
+ EventPeriod otherEventPeriod = (EventPeriod) other;
+ return otherEventPeriod.start.isEqual(this.start) && otherEventPeriod.end.isEqual(this.end);
+ }
+
+ @Override
+ public String toString() {
+ return "start: "
+ + this.start.format(DATE_TIME_STRING_FORMATTER)
+ + "; end: "
+ + this.end.format(DATE_TIME_STRING_FORMATTER);
+ }
+}
diff --git a/src/main/java/seedu/address/model/event/SingleDayEventList.java b/src/main/java/seedu/address/model/event/SingleDayEventList.java
new file mode 100644
index 00000000000..8885371a255
--- /dev/null
+++ b/src/main/java/seedu/address/model/event/SingleDayEventList.java
@@ -0,0 +1,183 @@
+package seedu.address.model.event;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Optional;
+import java.util.TreeMap;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import seedu.address.model.event.exceptions.ConflictingEventException;
+import seedu.address.model.event.exceptions.EventNotFoundException;
+
+/**
+ * Stores the event for a single day.
+ */
+public class SingleDayEventList {
+ private final LocalDate date;
+ private final NavigableMap eventTree;
+
+ /**
+ * Constructs a SingleDayEventList to store events for the day.
+ */
+ public SingleDayEventList(LocalDate date) {
+ requireNonNull(date);
+
+ this.date = date;
+ this.eventTree = new TreeMap(EventPeriod::compareTo);
+ }
+
+ /**
+ * Checks if adding an event is valid without causing conflicts with existing events.
+ *
+ * @param event The event to be added.
+ * @return True if the event can be added without conflicts, false otherwise.
+ */
+ public boolean isEventAddValid(Event event) {
+ requireNonNull(event);
+
+ EventPeriod eventPeriod = event.getEventPeriod();
+
+ Event precedingEvent = Optional.>ofNullable(this.eventTree.floorEntry(
+ eventPeriod)).map(Map.Entry::getValue).orElse(Event.createNonConflictingEvent());
+ Event proceedingEvent = Optional.>ofNullable(this.eventTree.ceilingEntry(
+ eventPeriod)).map(Map.Entry::getValue).orElse(Event.createNonConflictingEvent());
+ return !(precedingEvent.isConflicting(event) || proceedingEvent.isConflicting(event));
+ }
+
+ /**
+ * Adds an event to the event tree.
+ *
+ * @param event The event to be added.
+ */
+ public void addEvent(Event event) {
+ requireNonNull(event);
+ if (!isEventAddValid(event)) {
+ throw new ConflictingEventException();
+ }
+ eventTree.put(event.getEventPeriod(), event);
+ }
+
+ /**
+ * Forcibly add the event to the event tree, disregarding time period conflict.
+ *
+ * @param event The event to be added.
+ */
+ public void forceAddEvent(Event event) {
+ requireNonNull(event);
+ eventTree.put(event.getEventPeriod(), event);
+ }
+
+ /**
+ * Check if the event exists in the list.
+ *
+ * @param event event to be checked.
+ * @return true if it exists in the list, false otherwise.
+ */
+ public boolean containsEvent(Event event) {
+ requireNonNull(event);
+ for (Event thisEvent : eventTree.values()) {
+ if (event.equals(thisEvent)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if there is an event at specified time
+ *
+ * @param dateTime the specified time
+ * @return an {@code @Optional} containing the event if there is an event at the time, empty optional otherwise.
+ */
+ public Optional eventAtTime(LocalDateTime dateTime) {
+ requireNonNull(dateTime);
+ for (Event event : eventTree.values()) {
+ Event parentEvent = event.getParentEvent();
+ if (parentEvent.isDuring(dateTime)) {
+ return Optional.of(event.getParentEvent());
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Looks for all events within a time range and returns a List containing these events.
+ *
+ * @param eventPeriod The specified time range represented by a {@code EventPeriod}
+ * @return A List object containing all events that are within the time range.
+ */
+ public List eventsInRange(EventPeriod eventPeriod) {
+ ArrayList output = new ArrayList();
+ for (Event thisEvent : eventTree.values()) {
+ if (thisEvent.isPeriodConflicting(eventPeriod)) {
+ output.add(thisEvent.getParentEvent());
+ }
+ }
+ return output;
+ }
+
+ /**
+ * Removes the event from the list. The event must exist in the list.
+ * @param toRemove event to be removed.
+ */
+ public void remove(Event toRemove) {
+ requireNonNull(toRemove);
+ for (Event thisEvent : this.eventTree.values()) {
+ if (toRemove.getParentEvent().equals(thisEvent.getParentEvent())) {
+ this.eventTree.remove(thisEvent.getEventPeriod(), thisEvent);
+ return;
+ }
+ }
+ throw new EventNotFoundException();
+ }
+
+ /**
+ * Returns the events for the day as an observableList.
+ *
+ * @return the events for the day as an observableList.
+ */
+ public ObservableList getDayEventList() {
+ return FXCollections.observableArrayList(this.eventTree.values()
+ .toArray(new Event[0]));
+ }
+
+ /**
+ * Checks if there are no events present in this list.
+ *
+ * @return true if there are no events present, false otherwise.
+ */
+ public boolean isEmpty() {
+ return eventTree.isEmpty();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof SingleDayEventList)) {
+ return false;
+ }
+
+ SingleDayEventList otherDay = (SingleDayEventList) other;
+ if (!(this.eventTree.size() == otherDay.eventTree.size())) {
+ return false;
+ }
+
+ for (Event thisEvent : this.eventTree.values()) {
+ if (!otherDay.containsEvent(thisEvent)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/seedu/address/model/event/exceptions/ConflictingEventException.java b/src/main/java/seedu/address/model/event/exceptions/ConflictingEventException.java
new file mode 100644
index 00000000000..f5b736011db
--- /dev/null
+++ b/src/main/java/seedu/address/model/event/exceptions/ConflictingEventException.java
@@ -0,0 +1,10 @@
+package seedu.address.model.event.exceptions;
+
+/**
+ * Signals that the operation will result in events that overlap in their time frame.
+ */
+public class ConflictingEventException extends RuntimeException {
+ public ConflictingEventException() {
+ super("Operation would result in events with overlapping timeframe");
+ }
+}
diff --git a/src/main/java/seedu/address/model/event/exceptions/DateOutOfBoundsException.java b/src/main/java/seedu/address/model/event/exceptions/DateOutOfBoundsException.java
new file mode 100644
index 00000000000..8c309c2528f
--- /dev/null
+++ b/src/main/java/seedu/address/model/event/exceptions/DateOutOfBoundsException.java
@@ -0,0 +1,8 @@
+package seedu.address.model.event.exceptions;
+
+/**
+ * Thrown when the date input to bound the {@code @EventPeriod} is outside the original bounds of the
+ * {@code @EventPeriod}.
+ */
+public class DateOutOfBoundsException extends RuntimeException {
+}
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..b1da908e10e
--- /dev/null
+++ b/src/main/java/seedu/address/model/event/exceptions/EventNotFoundException.java
@@ -0,0 +1,7 @@
+package seedu.address.model.event.exceptions;
+
+/**
+ * Signals that the event could not be found in the calendar.
+ */
+public class EventNotFoundException extends RuntimeException {
+}
diff --git a/src/main/java/seedu/address/model/event/exceptions/InvalidDescriptionException.java b/src/main/java/seedu/address/model/event/exceptions/InvalidDescriptionException.java
new file mode 100644
index 00000000000..7f7dcd020ec
--- /dev/null
+++ b/src/main/java/seedu/address/model/event/exceptions/InvalidDescriptionException.java
@@ -0,0 +1,7 @@
+package seedu.address.model.event.exceptions;
+
+/**
+ * This exception is thrown when an eventDescription has a description that is invalid.
+ */
+public class InvalidDescriptionException extends RuntimeException {
+}
diff --git a/src/main/java/seedu/address/model/event/exceptions/InvalidEventPeriodException.java b/src/main/java/seedu/address/model/event/exceptions/InvalidEventPeriodException.java
new file mode 100644
index 00000000000..9db6ad01507
--- /dev/null
+++ b/src/main/java/seedu/address/model/event/exceptions/InvalidEventPeriodException.java
@@ -0,0 +1,14 @@
+package seedu.address.model.event.exceptions;
+
+
+/**
+ * This exception is thrown when an event period is invalid.
+ */
+public class InvalidEventPeriodException extends RuntimeException {
+ /**
+ * @param message should contain relevant information on the failed constraint(s)
+ */
+ public InvalidEventPeriodException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java
index abe8c46b535..3a331d22cb8 100644
--- a/src/main/java/seedu/address/model/person/Person.java
+++ b/src/main/java/seedu/address/model/person/Person.java
@@ -2,12 +2,17 @@
import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
+import javafx.collections.ObservableList;
import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.calendar.UniMateCalendar;
+import seedu.address.model.event.Event;
import seedu.address.model.tag.Tag;
/**
@@ -24,8 +29,10 @@ public class Person {
// Data fields
private final Address address;
private final Set tags = new HashSet<>();
+ private final UniMateCalendar calendar = new UniMateCalendar();
/**
+ * Primary constructor for a person object.
* Every field must be present and not null.
*/
public Person(Name name, Phone phone, Email email, Address address, Set tags) {
@@ -37,6 +44,22 @@ public Person(Name name, Phone phone, Email email, Address address, Set tag
this.tags.addAll(tags);
}
+ /**
+ * Alternative constructor to be used when replacing the calendar of a contact.
+ * Every field must be present and not null.
+ *
+ */
+ public Person(Name name, Phone phone, Email email, Address address, Set tags, UniMateCalendar calendar) {
+ requireAllNonNull(name, phone, email, address, tags);
+ this.name = name;
+ this.phone = phone;
+ this.email = email;
+ this.address = address;
+ this.tags.addAll(tags);
+ this.calendar.resetData(calendar);
+ }
+
+
public Name getName() {
return name;
}
@@ -53,6 +76,21 @@ public Address getAddress() {
return address;
}
+ public UniMateCalendar getCalendar() {
+ return calendar;
+ }
+
+ public ReadOnlyCalendar getReadOnlyCalendar() {
+ return calendar;
+ }
+
+ /**
+ * Returns a view of the events belonging to this person
+ */
+ public ObservableList getEventList() {
+ return calendar.getEventList();
+ }
+
/**
* Returns an immutable tag set, which throws {@code UnsupportedOperationException}
* if modification is attempted.
@@ -74,6 +112,44 @@ public boolean isSamePerson(Person otherPerson) {
&& otherPerson.getName().equals(getName());
}
+ /**
+ * Adds an event to the calendar of this Person.
+ */
+ public void addEvent(Event toAdd) {
+ calendar.addEvent(toAdd);
+ }
+
+ /**
+ * Checks if an event can be added to the calendar of this Person.
+ */
+ public boolean canAddEvent(Event toAdd) {
+ return calendar.canAddEvent(toAdd);
+ }
+
+ /**
+ * Deletes an event at the specified time from the calendar of this person.
+ */
+ public void deleteEvent(LocalDateTime targetTime) {
+ calendar.deleteEventAt(targetTime);
+ }
+
+ /**
+ * Looks for an event at the specified time and returns it.
+ */
+ public Event findEvent(LocalDateTime targetTime) {
+ return calendar.findEventAt(targetTime).get();
+ }
+
+ /**
+ * Checks if the person is tagged with the input tag.
+ *
+ * @param tag input tag to be checked.
+ * @return true if this person is tagged with the input tag.
+ */
+ public boolean hasTag(Tag tag) {
+ return getTags().stream().anyMatch(tag::equals);
+ }
+
/**
* Returns true if both persons have the same identity and data fields.
* This defines a stronger notion of equality between two persons.
diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java
index cc0a68d79f9..46c58cfd95a 100644
--- a/src/main/java/seedu/address/model/person/UniquePersonList.java
+++ b/src/main/java/seedu/address/model/person/UniquePersonList.java
@@ -3,11 +3,13 @@
import static java.util.Objects.requireNonNull;
import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
+import javafx.collections.transformation.SortedList;
import seedu.address.model.person.exceptions.DuplicatePersonException;
import seedu.address.model.person.exceptions.PersonNotFoundException;
@@ -97,6 +99,14 @@ public void setPersons(List persons) {
internalList.setAll(persons);
}
+ /**
+ * Sort persons in Person List according to comparator provided.
+ * @param comparator
+ */
+ public void sortPersons(Comparator comparator) {
+ internalList.setAll(new SortedList(internalList, comparator));
+ }
+
/**
* Returns the backing list as an unmodifiable {@code ObservableList}.
*/
diff --git a/src/main/java/seedu/address/model/person/comparer/AddressComparator.java b/src/main/java/seedu/address/model/person/comparer/AddressComparator.java
new file mode 100644
index 00000000000..88b9d3c6585
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/comparer/AddressComparator.java
@@ -0,0 +1,40 @@
+package seedu.address.model.person.comparer;
+
+import seedu.address.model.person.Person;
+
+/**
+ * A comparator that sorts Person objects by address.
+ */
+public class AddressComparator extends SortComparator {
+ /**
+ * The sort type for this comparator.
+ */
+ public static final String SORT_TYPE = "address";
+
+ /**
+ * Creates a new AddressComparator with the given parameters.
+ *
+ * @param isActive Whether this comparator is currently active.
+ * @param isReverse Whether to sort in reverse order.
+ * @param priority The priority of this comparator.
+ */
+ public AddressComparator(boolean isActive, boolean isReverse, int priority) {
+ super(isActive, isReverse, priority);
+ }
+
+ /**
+ * Compares two Person objects by address.
+ *
+ * @param p1 The first Person to compare.
+ * @param p2 The second Person to compare.
+ * @return A negative integer, zero, or a positive integer as the first argument is less than,
+ * equal to, or greater than the second.
+ */
+ @Override
+ public int compare(Person p1, Person p2) {
+ if (p1.equals(p2)) {
+ return 0;
+ }
+ return p1.getAddress().value.toLowerCase().compareTo(p2.getAddress().value.toLowerCase());
+ }
+}
diff --git a/src/main/java/seedu/address/model/person/comparer/EmailComparator.java b/src/main/java/seedu/address/model/person/comparer/EmailComparator.java
new file mode 100644
index 00000000000..314a0a0064d
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/comparer/EmailComparator.java
@@ -0,0 +1,41 @@
+package seedu.address.model.person.comparer;
+
+import seedu.address.model.person.Person;
+
+/**
+ * A comparator that sorts Person objects by email.
+ */
+public class EmailComparator extends SortComparator {
+
+ /**
+ * The sort type for this comparator.
+ */
+ public static final String SORT_TYPE = "email";
+
+ /**
+ * Creates a new AddressComparator with the given parameters.
+ *
+ * @param isActive Whether this comparator is currently active.
+ * @param isReverse Whether to sort in reverse order.
+ * @param priority The priority of this comparator.
+ */
+ public EmailComparator(boolean isActive, boolean isReverse, int priority) {
+ super(isActive, isReverse, priority);
+ }
+
+ /**
+ * Compares two Person objects by email.
+ *
+ * @param p1 The first Person to compare.
+ * @param p2 The second Person to compare.
+ * @return A negative integer, zero, or a positive integer as the first argument is less than,
+ * equal to, or greater than the second.
+ */
+ @Override
+ public int compare(Person p1, Person p2) {
+ if (p1.equals(p2)) {
+ return 0;
+ }
+ return p1.getEmail().value.compareTo(p2.getEmail().value);
+ }
+}
diff --git a/src/main/java/seedu/address/model/person/comparer/NameComparator.java b/src/main/java/seedu/address/model/person/comparer/NameComparator.java
new file mode 100644
index 00000000000..87d80c3e1a0
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/comparer/NameComparator.java
@@ -0,0 +1,41 @@
+package seedu.address.model.person.comparer;
+
+import seedu.address.model.person.Person;
+
+/**
+ * A comparator that sorts Person objects by name.
+ */
+public class NameComparator extends SortComparator {
+
+ /**
+ * The sort type for this comparator.
+ */
+ public static final String SORT_TYPE = "name";
+
+ /**
+ * Creates a new NameComparator with the given parameters.
+ *
+ * @param isActive Whether this comparator is currently active.
+ * @param isReverse Whether to sort in reverse order.
+ * @param priority The priority of this comparator.
+ */
+ public NameComparator(boolean isActive, boolean isReverse, int priority) {
+ super(isActive, isReverse, priority);
+ }
+
+ /**
+ * Compares two Person objects by name.
+ *
+ * @param p1 The first Person to compare.
+ * @param p2 The second Person to compare.
+ * @return A negative integer, zero, or a positive integer as the first argument is less than,
+ * equal to, or greater than the second.
+ */
+ @Override
+ public int compare(Person p1, Person p2) {
+ if (p1.equals(p2)) {
+ return 0;
+ }
+ return p1.getName().fullName.toLowerCase().compareTo(p2.getName().fullName.toLowerCase());
+ }
+}
diff --git a/src/main/java/seedu/address/model/person/comparer/PhoneComparator.java b/src/main/java/seedu/address/model/person/comparer/PhoneComparator.java
new file mode 100644
index 00000000000..ea512574cb7
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/comparer/PhoneComparator.java
@@ -0,0 +1,40 @@
+package seedu.address.model.person.comparer;
+
+import seedu.address.model.person.Person;
+
+/**
+ * A comparator that sorts Person objects by phone number.
+ */
+public class PhoneComparator extends SortComparator {
+ /**
+ * The sort type for this comparator.
+ */
+ public static final String SORT_TYPE = "phone";
+
+ /**
+ * Creates a new PhoneComparator with the given parameters.
+ *
+ * @param isActive Whether this comparator is currently active.
+ * @param isReverse Whether to sort in reverse order.
+ * @param priority The priority of this comparator.
+ */
+ public PhoneComparator(boolean isActive, boolean isReverse, int priority) {
+ super(isActive, isReverse, priority);
+ }
+
+ /**
+ * Compares two Person objects by phone number.
+ *
+ * @param p1 The first Person to compare.
+ * @param p2 The second Person to compare.
+ * @return A negative integer, zero, or a positive integer as the first argument is less than,
+ * equal to, or greater than the second.
+ */
+ @Override
+ public int compare(Person p1, Person p2) {
+ if (p1.equals(p2)) {
+ return 0;
+ }
+ return p1.getPhone().value.compareTo(p2.getPhone().value);
+ }
+}
diff --git a/src/main/java/seedu/address/model/person/comparer/SortComparator.java b/src/main/java/seedu/address/model/person/comparer/SortComparator.java
new file mode 100644
index 00000000000..2bc1894d5f6
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/comparer/SortComparator.java
@@ -0,0 +1,84 @@
+package seedu.address.model.person.comparer;
+
+import java.util.Comparator;
+
+import seedu.address.model.person.Person;
+
+/**
+ * An abstract class that defines a comparator for sorting Person objects.
+ */
+public abstract class SortComparator implements Comparator {
+
+ /**
+ * Whether this comparator is currently active.
+ */
+ protected boolean isActive;
+
+ /**
+ * Whether to sort in reverse order.
+ */
+ protected boolean isReverse;
+
+ /**
+ * The priority of this comparator.
+ */
+ protected int priority;
+
+ /**
+ * Creates a new SortComparator with the given parameters.
+ *
+ * @param isActive Whether this comparator is currently active.
+ * @param isReverse Whether to sort in reverse order.
+ * @param priority The priority of this comparator.
+ */
+ public SortComparator(boolean isActive, boolean isReverse, int priority) {
+ this.isActive = isActive;
+ this.isReverse = isReverse;
+ this.priority = isActive ? priority : -1;
+ }
+
+ /**
+ * Returns whether this comparator is currently active.
+ *
+ * @return Whether this comparator is currently active.
+ */
+ public boolean getIsActive() {
+ return this.isActive;
+ }
+
+ /**
+ * Returns whether to sort in reverse order.
+ *
+ * @return Whether to sort in reverse order.
+ */
+ public boolean getIsReverse() {
+ return this.isReverse;
+ }
+
+ /**
+ * Sets whether to sort in reverse order
+ *
+ */
+ public void setIsReverse(boolean isReverse) {
+ this.isReverse = isReverse;
+ }
+
+ /**
+ * Returns the priority of this comparator.
+ *
+ * @return The priority of this comparator.
+ */
+ public int getPriority() {
+ return this.priority;
+ }
+
+ /**
+ * Compares two Person objects using this comparator's sorting criteria.
+ *
+ * @param p1 The first Person to compare.
+ * @param p2 The second Person to compare.
+ * @return A negative integer, zero, or a positive integer as the first argument is less than,
+ * equal to, or greater than the second.
+ */
+ public abstract int compare(Person p1, Person p2);
+}
diff --git a/src/main/java/seedu/address/model/task/Deadline.java b/src/main/java/seedu/address/model/task/Deadline.java
new file mode 100644
index 00000000000..a8f37fdc2c3
--- /dev/null
+++ b/src/main/java/seedu/address/model/task/Deadline.java
@@ -0,0 +1,127 @@
+package seedu.address.model.task;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.ParserUtil.DATE_TIME_STRING_FORMATTER;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeParseException;
+import java.util.Optional;
+
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.model.task.exceptions.InvalidDeadlineException;
+
+/**
+ * An object representing the deadline of a task.
+ */
+public class Deadline implements Comparable {
+ public static final String MESSAGE_CONSTRAINTS = "The deadline should "
+ + "be in the format 'yyyy-MM-dd HH:mm' where:\n"
+ + " -'yyyy' is the year.\n"
+ + " -'MM' is the month.\n"
+ + " -'dd' is the day.\n"
+ + " -'HH:mm' is the time in 24-hour format.";
+
+ public static final String ABSENT_DEADLINE = "-";
+
+
+ private final Optional deadline;
+
+ /**
+ * Constructs a Deadline object with String representation of a datetime.
+ * If is String is null, instead creates an empty deadline.
+ *
+ * @param deadline the String representation of the deadline datetime.
+ */
+ public Deadline(String deadline) {
+ if (deadline == null) {
+ this.deadline = Optional.empty();
+ } else {
+ try {
+ this.deadline = Optional.of(LocalDateTime.parse(deadline, DATE_TIME_STRING_FORMATTER));
+ } catch (DateTimeParseException e) {
+ throw new InvalidDeadlineException();
+ }
+ }
+ }
+
+ /**
+ * Constructs a Deadline object with the provided LocalDateTime.
+ * If the LocalDateTime is null, instead creates an empty deadline.
+ *
+ * @param deadline the LocalDateTime.
+ */
+ public Deadline(LocalDateTime deadline) {
+ this.deadline = Optional.ofNullable(deadline);
+ }
+
+ /**
+ * Checks if a deadline is present
+ *
+ * @return true if a deadline is present, false otherwise.
+ */
+ public boolean isPresent() {
+ return deadline.isPresent();
+ }
+
+ public LocalDateTime getDeadline() {
+ return deadline.orElse(null);
+ }
+
+ /**
+ * Compares this deadline to another deadline. Deadlines that are present are prioritized above absent ones.
+ *
+ * @param other the object to be compared.
+ * @return -1 if this deadline is before, 1 if deadline is after and 0 if the deadlines are both present and equal.
+ */
+ @Override
+ public int compareTo(Deadline other) {
+ requireNonNull(other);
+
+ if (isPresent() && other.isPresent()) {
+ return deadline.get().compareTo(other.deadline.get());
+ } else if (isPresent()) {
+ return -1;
+ } else if (other.isPresent()) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Gets the formatted string representation of the deadline.
+ *
+ * @return the string representation of "-" if no deadline is present.
+ */
+ public String getFormattedDeadline() {
+ if (isPresent()) {
+ return deadline.get().format(DATE_TIME_STRING_FORMATTER);
+ }
+ return ABSENT_DEADLINE;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ requireNonNull(other);
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof Deadline)) {
+ return false;
+ }
+
+ Deadline otherDeadline = (Deadline) other;
+ if (isPresent() && otherDeadline.isPresent()) {
+ return deadline.get().equals(otherDeadline.deadline.get());
+ }
+ return isPresent() == otherDeadline.isPresent();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("deadline", deadline)
+ .toString();
+ }
+}
diff --git a/src/main/java/seedu/address/model/task/ReadOnlyTaskManager.java b/src/main/java/seedu/address/model/task/ReadOnlyTaskManager.java
new file mode 100644
index 00000000000..d69fc3dc991
--- /dev/null
+++ b/src/main/java/seedu/address/model/task/ReadOnlyTaskManager.java
@@ -0,0 +1,16 @@
+package seedu.address.model.task;
+
+import javafx.collections.ObservableList;
+
+/**
+ * A read only version of Task Manager.
+ */
+public interface ReadOnlyTaskManager {
+ /**
+ * Generates an unmodifiable view of the task list.
+ *
+ * @return unmodifiable view of the task list.
+ */
+ ObservableList getTaskList();
+
+}
diff --git a/src/main/java/seedu/address/model/task/Task.java b/src/main/java/seedu/address/model/task/Task.java
new file mode 100644
index 00000000000..823f241f1b2
--- /dev/null
+++ b/src/main/java/seedu/address/model/task/Task.java
@@ -0,0 +1,114 @@
+package seedu.address.model.task;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Comparator;
+
+import seedu.address.commons.util.ToStringBuilder;
+
+/**
+ * Represents a task in the task list.
+ */
+public class Task {
+ public static final DateTimeFormatter DATE_TIME_STRING_FORMATTER = DateTimeFormatter.ofPattern(
+ "yyyy-MM-dd HH:mm");
+ private final TaskDescription description;
+ private final Deadline deadline;
+
+ /**
+ * Constructs a Task object with the given description and deadline
+ */
+ public Task(String description, LocalDateTime deadline) {
+ requireNonNull(description);
+
+ this.description = new TaskDescription(description);
+ this.deadline = new Deadline(deadline);
+ }
+
+ /**
+ * Constructs a Task object with the given {@code TaskDescription} and {@code Deadline}
+ */
+ public Task(TaskDescription description, Deadline deadline) {
+ requireAllNonNull(description, deadline);
+
+ this.description = description;
+ this.deadline = deadline;
+ }
+
+ public TaskDescription getDescription() {
+ return description;
+ }
+
+ public String getDescriptionString() {
+ return description.get();
+ }
+
+ public Deadline getDeadline() {
+ return deadline;
+ }
+
+ public String getDeadlineString() {
+ return deadline.getFormattedDeadline();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof Task)) {
+ return false;
+ }
+
+ Task otherTask = (Task) other;
+ boolean descriptionMatches = description.equals(otherTask.description);
+ boolean deadlineMatches = deadline.equals(otherTask.deadline);
+ return descriptionMatches && deadlineMatches;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .add("description", description)
+ .add("deadline", deadline)
+ .toString();
+ }
+
+ /**
+ * A comparator to sort the task list by deadline.
+ */
+ public static class TaskDeadlineComparator implements Comparator {
+ @Override
+ public int compare(Task task1, Task task2) {
+ int firstResult = task1.deadline.compareTo(task2.deadline);
+ if (firstResult == 0) {
+ return new TaskDescriptorComparator().compare(task1, task2);
+ }
+ return firstResult;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof TaskDeadlineComparator;
+ }
+ }
+
+ /**
+ * A comparator to sort the task list by description, alphabetically.
+ */
+ public static class TaskDescriptorComparator implements Comparator {
+ public int compare(Task task1, Task task2) {
+ return task1.getDescription().compareTo(task2.getDescription());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof TaskDescriptorComparator;
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/model/task/TaskDescription.java b/src/main/java/seedu/address/model/task/TaskDescription.java
new file mode 100644
index 00000000000..9a6b908ac48
--- /dev/null
+++ b/src/main/java/seedu/address/model/task/TaskDescription.java
@@ -0,0 +1,63 @@
+package seedu.address.model.task;
+
+import static java.util.Objects.requireNonNull;
+
+import seedu.address.model.task.exceptions.InvalidTaskDescriptionException;
+
+/**
+ * Represents the description of a task.
+ */
+public class TaskDescription implements Comparable {
+ public static final String MESSAGE_CONSTRAINTS = "Task description cannot be empty.";
+ private final String description;
+
+ /**
+ * Constructs a TaskDescription object with the given description.
+ *
+ * @param description A String representing the description of the task.
+ */
+ public TaskDescription(String description) {
+ requireNonNull(description);
+ if (!isValidDescription(description)) {
+ throw new InvalidTaskDescriptionException();
+ }
+ this.description = description;
+ }
+
+ /**
+ * Checks if the given string is valid as a TaskDescription.
+ *
+ * @param description the description String to be checked.
+ * @return true if the description is valid, false otherwise.
+ */
+ public static boolean isValidDescription(String description) {
+ requireNonNull(description);
+ return !description.isEmpty();
+ }
+
+ /**
+ * Returns the string representation of the description.
+ */
+ public String get() {
+ return description;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof TaskDescription)) {
+ return false;
+ }
+
+ TaskDescription otherTaskDescription = (TaskDescription) other;
+ return description.equals(otherTaskDescription.description);
+ }
+
+ @Override
+ public int compareTo(TaskDescription other) {
+ return description.toLowerCase().compareTo(other.description.toLowerCase());
+ }
+}
diff --git a/src/main/java/seedu/address/model/task/TaskList.java b/src/main/java/seedu/address/model/task/TaskList.java
new file mode 100644
index 00000000000..3eb2f18717c
--- /dev/null
+++ b/src/main/java/seedu/address/model/task/TaskList.java
@@ -0,0 +1,135 @@
+package seedu.address.model.task;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import seedu.address.model.task.exceptions.DuplicateTaskException;
+import seedu.address.model.task.exceptions.TaskNotFoundException;
+
+/**
+ * A list of tasks that enforces uniqueness between its elements and does not allow nulls.
+ */
+public class TaskList implements Iterable {
+ private final ObservableList internalList = FXCollections.observableArrayList();
+ private final ObservableList internalUnmodifiableList =
+ FXCollections.unmodifiableObservableList(internalList);
+
+
+ /**
+ * Returns true if the list contains an equivalent task as the given argument.
+ */
+ public boolean contains(Task toCheck) {
+ requireNonNull(toCheck);
+ return internalList.stream().anyMatch(toCheck::equals);
+ }
+
+ /**
+ * Adds a task to the list.
+ * An equivalent task must not already exist in the list.
+ */
+ public void add(Task task) {
+ requireNonNull(task);
+ if (contains(task)) {
+ throw new DuplicateTaskException();
+ }
+ internalList.add(task);
+ }
+
+ /**
+ * Removes the equivalent task from the list.
+ * The task must exist in the list.
+ */
+ public void remove(Task toRemove) {
+ requireNonNull(toRemove);
+ if (!internalList.remove(toRemove)) {
+ throw new TaskNotFoundException();
+ }
+ }
+
+ /**
+ * Removes a task at specified index from the list and returns the removed task.
+ */
+ public Task remove(int index) {
+ requireNonNull(index);
+
+ return internalList.remove(index);
+ }
+
+ public void setTasks(TaskList replacement) {
+ requireNonNull(replacement);
+ internalList.setAll(replacement.internalList);
+ }
+
+ /**
+ * Replaces the contents of this list with {@code tasks}.
+ * {@code tasks} must not contain duplicate tasks.
+ */
+ public void setTasks(List tasks) {
+ requireAllNonNull(tasks);
+ if (!tasksAreUnique(tasks)) {
+ throw new DuplicateTaskException();
+ }
+ internalList.setAll(tasks);
+ }
+
+ /**
+ * Sorts tasks in the list according to the current sorting order setting of the list.
+ */
+ public void sortTasks(Comparator sortingOrder) {
+ internalList.sort(sortingOrder);
+ }
+
+
+ /**
+ * Returns the backing list as an unmodifiable {@code ObservableList}.
+ */
+ public ObservableList asUnmodifiableList() {
+ return internalUnmodifiableList;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ // instanceof handles nulls
+ if (!(other instanceof TaskList)) {
+ return false;
+ }
+
+ TaskList otherTaskList = (TaskList) other;
+ return internalList.equals(otherTaskList.internalList);
+ }
+
+ /**
+ * Returns true if {@code tasks} contains only unique tasks.
+ */
+ private boolean tasksAreUnique(List tasks) {
+ for (int i = 0; i < tasks.size() - 1; i++) {
+ for (int j = i + 1; j < tasks.size(); j++) {
+ if (tasks.get(i).equals(tasks.get(j))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return internalList.toString();
+ }
+
+
+ @Override
+ public Iterator iterator() {
+ return internalList.iterator();
+ }
+}
diff --git a/src/main/java/seedu/address/model/task/TaskManager.java b/src/main/java/seedu/address/model/task/TaskManager.java
new file mode 100644
index 00000000000..d69b3c756af
--- /dev/null
+++ b/src/main/java/seedu/address/model/task/TaskManager.java
@@ -0,0 +1,141 @@
+package seedu.address.model.task;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Comparator;
+import java.util.List;
+
+import javafx.collections.ObservableList;
+import seedu.address.model.task.exceptions.InvalidSortingOrderException;
+
+/**
+ * Represents a task manager that stores and manages tasks
+ */
+public class TaskManager implements ReadOnlyTaskManager {
+ private static final String COMPARATOR_TYPE_DESCRIPTION = "Description";
+ private static final String COMPARATOR_TYPE_DEADLINE = "Deadline";
+
+ private final TaskList tasks;
+ private Comparator sortingOrder;
+
+ /**
+ * Constructs a TaskManager object with an empty task list.
+ */
+ public TaskManager() {
+ this.tasks = new TaskList();
+ this.sortingOrder = new Task.TaskDeadlineComparator();
+ }
+
+ /**
+ * Constructs a TaskManager using the tasks in the {@code toBeCopied}
+ */
+ public TaskManager(ReadOnlyTaskManager toBeCopied) {
+ this();
+ requireNonNull(toBeCopied);
+ resetData(toBeCopied);
+ }
+
+ /**
+ * Resets the existing data of this {@code TaskManager} with {@code newData}.
+ */
+ public void resetData(ReadOnlyTaskManager newData) {
+ requireNonNull(newData);
+
+ setTasks(newData.getTaskList());
+ }
+
+ @Override
+ public ObservableList getTaskList() {
+ return tasks.asUnmodifiableList();
+ }
+
+ /**
+ * Replaces the contents of the taskList with {@code taskList}.
+ * {@code taskList} must not contain duplicate tasks.
+ */
+ public void setTasks(List taskList) {
+ requireNonNull(taskList);
+
+ tasks.setTasks(taskList);
+ }
+
+ /**
+ * Returns true if a person with the same identity as {@code person} exists in the address book.
+ */
+ public boolean hasTask(Task task) {
+ requireNonNull(task);
+ return tasks.contains(task);
+ }
+
+ /**
+ * Adds a task into the taskManager.
+ */
+ public void addTask(Task task) {
+ requireNonNull(task);
+ tasks.add(task);
+ sort();
+ }
+
+ public Task deleteTask(int index) {
+ return tasks.remove(index);
+ }
+
+ /**
+ * Sets the sorting order setting to sort by deadline.
+ */
+ private void setSortDeadline() {
+ sortingOrder = new Task.TaskDeadlineComparator();
+ sort();
+ }
+
+ /**
+ * Sets the sorting order setting to sort by description.
+ */
+ private void setSortDescription() {
+ sortingOrder = new Task.TaskDescriptorComparator();
+ sort();
+ }
+
+ /**
+ * Returns the sorting order.
+ */
+ public Comparator getSortingOrder() {
+ return sortingOrder;
+ }
+
+ /**
+ * Sets the sorting order of the task list to the specified comparator type.
+ */
+ public void sortTasksBy(String comparatorType) {
+ if (comparatorType.equalsIgnoreCase(COMPARATOR_TYPE_DESCRIPTION)) {
+ setSortDescription();
+ } else if (comparatorType.equalsIgnoreCase(COMPARATOR_TYPE_DEADLINE)) {
+ setSortDeadline();;
+ } else {
+ throw new InvalidSortingOrderException();
+ }
+ }
+
+ /**
+ * Sorts tasks in the task list according to the current sorting order.
+ */
+ public void sort() {
+ tasks.sortTasks(sortingOrder);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof TaskManager)) {
+ return false;
+ }
+
+ TaskManager otherTaskManager = (TaskManager) other;
+ boolean tasksMatch = this.tasks.equals(otherTaskManager.tasks);
+ boolean sortOrderMatch = this.sortingOrder.equals(otherTaskManager.sortingOrder);
+ return tasksMatch && sortOrderMatch;
+ }
+}
diff --git a/src/main/java/seedu/address/model/task/exceptions/DuplicateTaskException.java b/src/main/java/seedu/address/model/task/exceptions/DuplicateTaskException.java
new file mode 100644
index 00000000000..fa8a65a0bc9
--- /dev/null
+++ b/src/main/java/seedu/address/model/task/exceptions/DuplicateTaskException.java
@@ -0,0 +1,7 @@
+package seedu.address.model.task.exceptions;
+
+/**
+ * Signals that there is a duplicate task in the task list.
+ */
+public class DuplicateTaskException extends RuntimeException{
+}
diff --git a/src/main/java/seedu/address/model/task/exceptions/InvalidDeadlineException.java b/src/main/java/seedu/address/model/task/exceptions/InvalidDeadlineException.java
new file mode 100644
index 00000000000..85e2324a8e5
--- /dev/null
+++ b/src/main/java/seedu/address/model/task/exceptions/InvalidDeadlineException.java
@@ -0,0 +1,7 @@
+package seedu.address.model.task.exceptions;
+
+/**
+ * An exception thrown when a string of improper format is used to create a Deadline.
+ */
+public class InvalidDeadlineException extends RuntimeException {
+}
diff --git a/src/main/java/seedu/address/model/task/exceptions/InvalidSortingOrderException.java b/src/main/java/seedu/address/model/task/exceptions/InvalidSortingOrderException.java
new file mode 100644
index 00000000000..60909f8973f
--- /dev/null
+++ b/src/main/java/seedu/address/model/task/exceptions/InvalidSortingOrderException.java
@@ -0,0 +1,7 @@
+package seedu.address.model.task.exceptions;
+
+/**
+ * An exception thrown when a sorting order provided is invalid.
+ */
+public class InvalidSortingOrderException extends RuntimeException {
+}
diff --git a/src/main/java/seedu/address/model/task/exceptions/InvalidTaskDescriptionException.java b/src/main/java/seedu/address/model/task/exceptions/InvalidTaskDescriptionException.java
new file mode 100644
index 00000000000..6daa8e53ed6
--- /dev/null
+++ b/src/main/java/seedu/address/model/task/exceptions/InvalidTaskDescriptionException.java
@@ -0,0 +1,7 @@
+package seedu.address.model.task.exceptions;
+
+/**
+ * An exception thrown when a task description is invalid.
+ */
+public class InvalidTaskDescriptionException extends RuntimeException {
+}
diff --git a/src/main/java/seedu/address/model/task/exceptions/TaskNotFoundException.java b/src/main/java/seedu/address/model/task/exceptions/TaskNotFoundException.java
new file mode 100644
index 00000000000..c5216c4ae57
--- /dev/null
+++ b/src/main/java/seedu/address/model/task/exceptions/TaskNotFoundException.java
@@ -0,0 +1,7 @@
+package seedu.address.model.task.exceptions;
+
+/**
+ * Signals that the task could not be found in the task list.
+ */
+public class TaskNotFoundException extends RuntimeException {
+}
diff --git a/src/main/java/seedu/address/model/util/SampleCalendarUtil.java b/src/main/java/seedu/address/model/util/SampleCalendarUtil.java
new file mode 100644
index 00000000000..c27f99682f3
--- /dev/null
+++ b/src/main/java/seedu/address/model/util/SampleCalendarUtil.java
@@ -0,0 +1,32 @@
+package seedu.address.model.util;
+
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.calendar.UniMateCalendar;
+import seedu.address.model.event.Event;
+import seedu.address.model.event.EventDescription;
+import seedu.address.model.event.EventPeriod;
+
+/**
+ * Contains utility methods for populating {@code Calendar} with sample data.
+ */
+public class SampleCalendarUtil {
+ public static Event[] getSampleEvents() {
+ return new Event[] {
+ new Event(new EventDescription("Weekly team meeting"),
+ new EventPeriod("2023-10-25 09:00", "2023-10-25 10:30")),
+ new Event(new EventDescription(" Annual health checkup"),
+ new EventPeriod("2023-11-10 15:15", "2023-11-10 16:00")),
+ new Event(new EventDescription(" Birthday celebration"),
+ new EventPeriod("2023-11-15 18:00", "2023-11-15 22:00"))
+ };
+ }
+
+ public static ReadOnlyCalendar getSampleCalendar() {
+ UniMateCalendar sampleCalendar = new UniMateCalendar();
+ for (Event sampleEvent : getSampleEvents()) {
+ sampleCalendar.addEvent(sampleEvent);
+ }
+ return sampleCalendar;
+ }
+
+}
diff --git a/src/main/java/seedu/address/model/util/SampleTaskManagerUtil.java b/src/main/java/seedu/address/model/util/SampleTaskManagerUtil.java
new file mode 100644
index 00000000000..e360cb7d185
--- /dev/null
+++ b/src/main/java/seedu/address/model/util/SampleTaskManagerUtil.java
@@ -0,0 +1,32 @@
+package seedu.address.model.util;
+
+import seedu.address.model.task.Deadline;
+import seedu.address.model.task.ReadOnlyTaskManager;
+import seedu.address.model.task.Task;
+import seedu.address.model.task.TaskDescription;
+import seedu.address.model.task.TaskManager;
+
+/**
+ * Contains utility methods for populating {@code TaskManager} with sample data.
+ */
+public class SampleTaskManagerUtil {
+ public static Task[] getSampleTasks() {
+ return new Task[] {
+ new Task(new TaskDescription("Caffeinate"),
+ new Deadline("2023-11-11 00:00")),
+ new Task(new TaskDescription("Buy a new dog"),
+ new Deadline((String) null)),
+ new Task(new TaskDescription("Sell textbooks"),
+ new Deadline("2023-12-31 00:00"))
+ };
+ }
+
+ public static ReadOnlyTaskManager getSampleTaskManager() {
+ TaskManager sampleTaskManager = new TaskManager();
+ for (Task sampleTask : getSampleTasks()) {
+ sampleTaskManager.addTask(sampleTask);
+ }
+ return sampleTaskManager;
+ }
+
+}
diff --git a/src/main/java/seedu/address/storage/CalendarStorage.java b/src/main/java/seedu/address/storage/CalendarStorage.java
new file mode 100644
index 00000000000..2aa09aebb85
--- /dev/null
+++ b/src/main/java/seedu/address/storage/CalendarStorage.java
@@ -0,0 +1,45 @@
+package seedu.address.storage;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+
+import seedu.address.commons.exceptions.DataLoadingException;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+
+/**
+ * Represents a storage for the Calendar.
+ */
+public interface CalendarStorage {
+
+ /**
+ * Returns the file path of the data file.
+ */
+ Path getCalendarFilePath();
+
+ /**
+ * Returns AddressBook data as a {@link ReadOnlyCalendar}.
+ * Returns {@code Optional.empty()} if storage file is not found.
+ *
+ * @throws DataLoadingException if loading the data from storage failed.
+ */
+ Optional readCalendar() throws DataLoadingException;
+
+ /**
+ * @see #getCalendarFilePath()
+ */
+ Optional readCalendar(Path filePath) throws DataLoadingException;
+
+ /**
+ * Saves the given {@link ReadOnlyCalendar} to the storage.
+ * @param calendar cannot be null.
+ * @throws IOException if there was any problem writing to the file.
+ */
+ void saveCalendar(ReadOnlyCalendar calendar) throws IOException;
+
+ /**
+ * @see #saveCalendar(ReadOnlyCalendar)
+ */
+ void saveCalendar(ReadOnlyCalendar addressBook, Path filePath) throws IOException;
+
+}
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..070886f84e1
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonAdaptedEvent.java
@@ -0,0 +1,81 @@
+package seedu.address.storage;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.logic.parser.ParserUtil;
+import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.event.Event;
+import seedu.address.model.event.EventDescription;
+import seedu.address.model.event.EventPeriod;
+
+/**
+ * Jackson-friendly version of {@link Event}.
+ */
+class JsonAdaptedEvent {
+
+ public static final String MISSING_FIELD_MESSAGE_FORMAT = "Event's %s field is missing!";
+ public static final String INVALID_FIELD_MESSAGE_FORMAT = EventPeriod.MESSAGE_CONSTRAINTS;
+
+ private final String description;
+ private final String eventPeriod;
+
+ /**
+ * Constructs a {@code JsonAdaptedEvent} with the given event details.
+ */
+ @JsonCreator
+ public JsonAdaptedEvent(@JsonProperty("description") String description,
+ @JsonProperty("eventPeriod") String eventPeriod) {
+ this.description = description;
+ this.eventPeriod = eventPeriod;
+ }
+
+ /**
+ * Converts a given {@code Event} into this class for Jackson use.
+ */
+ public JsonAdaptedEvent(Event source) {
+ description = source.getDescriptionString();
+ eventPeriod = source.getEventPeriodString();
+ }
+
+ /**
+ * Converts this Jackson-friendly adapted person object into the model's {@code Event} object.
+ *
+ * @throws IllegalValueException if there were any data constraints violated in the adapted event.
+ */
+ public Event toModelType() throws IllegalValueException {
+ if (description == null) {
+ throw new IllegalValueException(
+ String.format(MISSING_FIELD_MESSAGE_FORMAT,
+ EventDescription.class.getSimpleName()));
+ }
+ if (!EventDescription.isValid(description)) {
+ throw new IllegalValueException(EventDescription.MESSAGE_CONSTRAINTS);
+ }
+ final EventDescription modelDescription = new EventDescription(description);
+
+ // Event Periods are expected to be saved in this format yyyy-MM-dd HH:mm - yyyy-MM-dd HH:mm
+ String start;
+ String end;
+ if (eventPeriod == null) {
+ throw new IllegalValueException(
+ String.format(MISSING_FIELD_MESSAGE_FORMAT,
+ EventPeriod.class.getSimpleName()));
+ }
+ String[] parts = eventPeriod.split(" - ");
+ if (parts.length == 2) {
+ start = parts[0];
+ end = parts[1];
+ } else {
+ throw new IllegalValueException(String.format(INVALID_FIELD_MESSAGE_FORMAT,
+ EventPeriod.class.getSimpleName()));
+ }
+ try {
+ final EventPeriod modelEventPeriod = ParserUtil.parseEventPeriod(start, end);
+ return new Event(modelDescription, modelEventPeriod);
+ } catch (ParseException pe) {
+ throw new IllegalValueException(pe.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java
index bd1ca0f56c8..55089dbc2ec 100644
--- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java
+++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java
@@ -10,6 +10,8 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.model.calendar.UniMateCalendar;
+import seedu.address.model.event.Event;
import seedu.address.model.person.Address;
import seedu.address.model.person.Email;
import seedu.address.model.person.Name;
@@ -23,12 +25,14 @@
class JsonAdaptedPerson {
public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!";
+ public static final String MESSAGE_DUPLICATE_EVENT = "Calendar contains duplicate event(s).";
private final String name;
private final String phone;
private final String email;
private final String address;
private final List tags = new ArrayList<>();
+ private final List events = new ArrayList<>();
/**
* Constructs a {@code JsonAdaptedPerson} with the given person details.
@@ -36,7 +40,7 @@ class JsonAdaptedPerson {
@JsonCreator
public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone,
@JsonProperty("email") String email, @JsonProperty("address") String address,
- @JsonProperty("tags") List tags) {
+ @JsonProperty("tags") List tags, @JsonProperty("events") List events) {
this.name = name;
this.phone = phone;
this.email = email;
@@ -44,6 +48,7 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone
if (tags != null) {
this.tags.addAll(tags);
}
+ this.events.addAll(events);
}
/**
@@ -57,6 +62,8 @@ public JsonAdaptedPerson(Person source) {
tags.addAll(source.getTags().stream()
.map(JsonAdaptedTag::new)
.collect(Collectors.toList()));
+ events.addAll(source.getEventList().stream().map(Event::getParentEvent).distinct()
+ .map(JsonAdaptedEvent::new).collect(Collectors.toList()));
}
/**
@@ -103,7 +110,17 @@ 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);
+
+ UniMateCalendar calendar = new UniMateCalendar();
+ for (JsonAdaptedEvent jsonAdaptedEvent : events) {
+ Event event = jsonAdaptedEvent.toModelType();
+ if (calendar.contains(event)) {
+ throw new IllegalValueException(MESSAGE_DUPLICATE_EVENT);
+ }
+ calendar.addEvent(event);
+ }
+
+ return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags, calendar);
}
}
diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTask.java b/src/main/java/seedu/address/storage/JsonAdaptedTask.java
new file mode 100644
index 00000000000..3c04b082879
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonAdaptedTask.java
@@ -0,0 +1,71 @@
+package seedu.address.storage;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.model.task.Deadline;
+import seedu.address.model.task.Task;
+import seedu.address.model.task.TaskDescription;
+import seedu.address.model.task.exceptions.InvalidDeadlineException;
+
+/**
+ * Jackson-friendly version of {@link Task}.
+ */
+class JsonAdaptedTask {
+
+ public static final String MISSING_FIELD_MESSAGE_FORMAT = "Task's %s field is missing!";
+
+ private final String description;
+ private final String deadline;
+
+ /**
+ * Constructs a {@code JsonAdaptedTask} with the given task details.
+ */
+ @JsonCreator
+ public JsonAdaptedTask(@JsonProperty("description") String description,
+ @JsonProperty("deadline") String deadline) {
+ this.description = description;
+ this.deadline = deadline;
+ }
+
+ /**
+ * Converts a given {@code Task} into this class for Jackson use.
+ */
+ public JsonAdaptedTask(Task source) {
+ description = source.getDescriptionString();
+ deadline = source.getDeadlineString();
+ }
+
+ /**
+ * Converts this Jackson-friendly adapted task object into the model's {@code Task} object.
+ *
+ * @throws IllegalValueException if there were any data constraints violated in the adapted task.
+ */
+ public Task toModelType() throws IllegalValueException {
+ if (description == null) {
+ throw new IllegalValueException(
+ String.format(MISSING_FIELD_MESSAGE_FORMAT,
+ TaskDescription.class.getSimpleName()));
+ }
+ if (!TaskDescription.isValidDescription(description)) {
+ throw new IllegalValueException(TaskDescription.MESSAGE_CONSTRAINTS);
+ }
+ final TaskDescription modelDescription = new TaskDescription(description);
+
+ // Deadlines are expected to be saved in this format yyyy-MM-dd HH:mm or - if there is no deadline
+ if (deadline == null) {
+ throw new IllegalValueException(
+ String.format(MISSING_FIELD_MESSAGE_FORMAT,
+ Deadline.class.getSimpleName()));
+ }
+ //Null argument provided if there is deadline is saved as "-" in Json file
+ try {
+ Deadline modelDeadline = deadline.equals(Deadline.ABSENT_DEADLINE)
+ ? new Deadline((String) null) : new Deadline(deadline);
+ return new Task(modelDescription, modelDeadline);
+ } catch (InvalidDeadlineException e) {
+ throw new IllegalValueException(Deadline.MESSAGE_CONSTRAINTS);
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/storage/JsonCalendarStorage.java b/src/main/java/seedu/address/storage/JsonCalendarStorage.java
new file mode 100644
index 00000000000..98405417432
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonCalendarStorage.java
@@ -0,0 +1,80 @@
+package seedu.address.storage;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+import seedu.address.commons.core.LogsCenter;
+import seedu.address.commons.exceptions.DataLoadingException;
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.commons.util.FileUtil;
+import seedu.address.commons.util.JsonUtil;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+
+/**
+ * A class to access Calendar data stored as a json file on the hard disk.
+ */
+public class JsonCalendarStorage implements CalendarStorage {
+
+ private static final Logger logger = LogsCenter.getLogger(JsonCalendarStorage.class);
+
+ private Path filePath;
+
+ public JsonCalendarStorage(Path filePath) {
+ this.filePath = filePath;
+ }
+
+ public Path getCalendarFilePath() {
+ return filePath;
+ }
+
+ @Override
+ public Optional readCalendar() throws DataLoadingException {
+ return readCalendar(filePath);
+ }
+
+ /**
+ * Similar to {@link #readCalendar()}.
+ *
+ * @param filePath location of the data. Cannot be null.
+ * @throws DataLoadingException if loading the data from storage failed.
+ */
+ public Optional readCalendar(Path filePath) throws DataLoadingException {
+ requireNonNull(filePath);
+
+ Optional jsonCalendar = JsonUtil.readJsonFile(
+ filePath, JsonSerializableCalendar.class);
+ if (!jsonCalendar.isPresent()) {
+ return Optional.empty();
+ }
+
+ try {
+ return Optional.of(jsonCalendar.get().toModelType());
+ } catch (IllegalValueException ive) {
+ logger.info("Illegal values found in " + filePath + ": " + ive.getMessage());
+ throw new DataLoadingException(ive);
+ }
+ }
+
+ @Override
+ public void saveCalendar(ReadOnlyCalendar calendar) throws IOException {
+ saveCalendar(calendar, filePath);
+ }
+
+ /**
+ * Similar to {@link #saveCalendar(ReadOnlyCalendar)}.
+ *
+ * @param filePath location of the data. Cannot be null.
+ */
+ public void saveCalendar(ReadOnlyCalendar calendar, Path filePath) throws IOException {
+ requireNonNull(calendar);
+ requireNonNull(filePath);
+
+ FileUtil.createIfMissing(filePath);
+ JsonUtil.saveJsonFile(new JsonSerializableCalendar(calendar), filePath);
+ }
+
+}
diff --git a/src/main/java/seedu/address/storage/JsonSerializableCalendar.java b/src/main/java/seedu/address/storage/JsonSerializableCalendar.java
new file mode 100644
index 00000000000..c23ecd5fd5f
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonSerializableCalendar.java
@@ -0,0 +1,60 @@
+package seedu.address.storage;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonRootName;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.calendar.UniMateCalendar;
+import seedu.address.model.event.Event;
+
+/**
+ * An Immutable Calendar that is serializable to JSON format.
+ */
+@JsonRootName(value = "calendar")
+class JsonSerializableCalendar {
+ public static final String MESSAGE_DUPLICATE_EVENT = "Calendar contains duplicate event(s).";
+
+ private final List events = new ArrayList<>();
+
+ /**
+ * Constructs a {@code JsonSerializableCalendar} with the given events.
+ */
+ @JsonCreator
+ public JsonSerializableCalendar(@JsonProperty("events") List events) {
+ this.events.addAll(events);
+ }
+
+ /**
+ * Converts a given {@code ReadOnlyCalendar} into this class for Jackson use.
+ *
+ * @param source future changes to this will not affect the created {@code JsonSerializableCalendar}.
+ */
+ public JsonSerializableCalendar(ReadOnlyCalendar source) {
+ events.addAll(source.getEventList().stream().map(Event::getParentEvent).distinct()
+ .map(JsonAdaptedEvent::new).collect(Collectors.toList()));
+ }
+
+ /**
+ * Converts this address book into the model's {@code AddressBook} object.
+ *
+ * @throws IllegalValueException if there were any data constraints violated.
+ */
+ public UniMateCalendar toModelType() throws IllegalValueException {
+ UniMateCalendar calendar = new UniMateCalendar();
+ for (JsonAdaptedEvent jsonAdaptedEvent : events) {
+ Event event = jsonAdaptedEvent.toModelType();
+ if (calendar.contains(event)) {
+ throw new IllegalValueException(MESSAGE_DUPLICATE_EVENT);
+ }
+ calendar.addEvent(event);
+ }
+ return calendar;
+ }
+
+}
diff --git a/src/main/java/seedu/address/storage/JsonSerializableTaskManager.java b/src/main/java/seedu/address/storage/JsonSerializableTaskManager.java
new file mode 100644
index 00000000000..dff767fd346
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonSerializableTaskManager.java
@@ -0,0 +1,60 @@
+package seedu.address.storage;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonRootName;
+
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.model.task.ReadOnlyTaskManager;
+import seedu.address.model.task.Task;
+import seedu.address.model.task.TaskManager;
+
+/**
+ * An Immutable TaskManager that is serializable to JSON format.
+ */
+@JsonRootName(value = "taskmanager")
+class JsonSerializableTaskManager {
+
+ public static final String MESSAGE_DUPLICATE_TASK = "Task list contains duplicate task(s).";
+
+ private final List tasks = new ArrayList<>();
+
+ /**
+ * Constructs a {@code JsonSerializableTaskManager} with the given tasks.
+ */
+ @JsonCreator
+ public JsonSerializableTaskManager(@JsonProperty("tasks") List tasks) {
+ this.tasks.addAll(tasks);
+ }
+
+ /**
+ * Converts a given {@code ReadOnlyTaskManager} into this class for Jackson use.
+ *
+ * @param source future changes to this will not affect the created {@code JsonSerializableTaskManager}.
+ */
+ public JsonSerializableTaskManager(ReadOnlyTaskManager source) {
+ tasks.addAll(source.getTaskList().stream().map(JsonAdaptedTask::new).collect(Collectors.toList()));
+ }
+
+ /**
+ * Converts this task manager into the model's {@code TaskManager} object.
+ *
+ * @throws IllegalValueException if there were any data constraints violated.
+ */
+ public TaskManager toModelType() throws IllegalValueException {
+ TaskManager taskManager = new TaskManager();
+ for (JsonAdaptedTask jsonAdaptedTask : tasks) {
+ Task task = jsonAdaptedTask.toModelType();
+ if (taskManager.hasTask(task)) {
+ throw new IllegalValueException(MESSAGE_DUPLICATE_TASK);
+ }
+ taskManager.addTask(task);
+ }
+ return taskManager;
+ }
+
+}
diff --git a/src/main/java/seedu/address/storage/JsonTaskManagerStorage.java b/src/main/java/seedu/address/storage/JsonTaskManagerStorage.java
new file mode 100644
index 00000000000..0a3b1b71ece
--- /dev/null
+++ b/src/main/java/seedu/address/storage/JsonTaskManagerStorage.java
@@ -0,0 +1,80 @@
+package seedu.address.storage;
+
+import static java.util.Objects.requireNonNull;
+import static seedu.address.commons.util.CollectionUtil.requireAllNonNull;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+import seedu.address.commons.core.LogsCenter;
+import seedu.address.commons.exceptions.DataLoadingException;
+import seedu.address.commons.exceptions.IllegalValueException;
+import seedu.address.commons.util.FileUtil;
+import seedu.address.commons.util.JsonUtil;
+import seedu.address.model.task.ReadOnlyTaskManager;
+
+/**
+ * A class to access TaskManager data stored as a json file on the hard disk.
+ */
+public class JsonTaskManagerStorage implements TaskManagerStorage {
+
+ private static final Logger logger = LogsCenter.getLogger(JsonTaskManagerStorage.class);
+
+ private Path filePath;
+
+ public JsonTaskManagerStorage(Path filePath) {
+ this.filePath = filePath;
+ }
+
+ public Path getTaskManagerFilePath() {
+ return filePath;
+ }
+
+ @Override
+ public Optional readTaskManager() throws DataLoadingException {
+ return readTaskManager(filePath);
+ }
+
+ /**
+ * Similar to {@link #readTaskManager()}.
+ *
+ * @param filePath location of the data. Cannot be null.
+ * @throws DataLoadingException if loading the data from storage failed.
+ */
+ public Optional readTaskManager(Path filePath) throws DataLoadingException {
+ requireNonNull(filePath);
+
+ Optional jsonTaskManager = JsonUtil.readJsonFile(
+ filePath, JsonSerializableTaskManager.class);
+ if (!jsonTaskManager.isPresent()) {
+ return Optional.empty();
+ }
+
+ try {
+ return Optional.of(jsonTaskManager.get().toModelType());
+ } catch (IllegalValueException ive) {
+ logger.info("Illegal values found in " + filePath + ": " + ive.getMessage());
+ throw new DataLoadingException(ive);
+ }
+ }
+
+ @Override
+ public void saveTaskManager(ReadOnlyTaskManager taskManager) throws IOException {
+ saveTaskManager(taskManager, filePath);
+ }
+
+ /**
+ * Similar to {@link #saveTaskManager(ReadOnlyTaskManager)}.
+ *
+ * @param filePath location of the data. Cannot be null.
+ */
+ public void saveTaskManager(ReadOnlyTaskManager taskManager, Path filePath) throws IOException {
+ requireAllNonNull(taskManager, filePath);
+
+ FileUtil.createIfMissing(filePath);
+ JsonUtil.saveJsonFile(new JsonSerializableTaskManager(taskManager), filePath);
+ }
+
+}
diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java
index 9fba0c7a1d6..f9a2357058a 100644
--- a/src/main/java/seedu/address/storage/Storage.java
+++ b/src/main/java/seedu/address/storage/Storage.java
@@ -8,11 +8,13 @@
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.model.ReadOnlyUserPrefs;
import seedu.address.model.UserPrefs;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.task.ReadOnlyTaskManager;
/**
* API of the Storage component
*/
-public interface Storage extends AddressBookStorage, UserPrefsStorage {
+public interface Storage extends AddressBookStorage, CalendarStorage, TaskManagerStorage, UserPrefsStorage {
@Override
Optional readUserPrefs() throws DataLoadingException;
@@ -29,4 +31,17 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage {
@Override
void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException;
+ @Override
+ Optional readCalendar() throws DataLoadingException;
+
+ @Override
+ void saveCalendar(ReadOnlyCalendar calendar) throws IOException;
+
+ @Override
+ Optional readTaskManager() throws DataLoadingException;
+
+ @Override
+ void saveTaskManager(ReadOnlyTaskManager taskManager) throws IOException;
+
+
}
diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java
index 8b84a9024d5..f1fd38ee7fc 100644
--- a/src/main/java/seedu/address/storage/StorageManager.java
+++ b/src/main/java/seedu/address/storage/StorageManager.java
@@ -10,6 +10,8 @@
import seedu.address.model.ReadOnlyAddressBook;
import seedu.address.model.ReadOnlyUserPrefs;
import seedu.address.model.UserPrefs;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.task.ReadOnlyTaskManager;
/**
* Manages storage of AddressBook data in local storage.
@@ -18,13 +20,21 @@ public class StorageManager implements Storage {
private static final Logger logger = LogsCenter.getLogger(StorageManager.class);
private AddressBookStorage addressBookStorage;
+ private CalendarStorage calendarStorage;
+ private TaskManagerStorage taskManagerStorage;
private UserPrefsStorage userPrefsStorage;
/**
- * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}.
+ * Creates a {@code StorageManager} with the given {@code AddressBookStorage},
+ * {@code CalendarStorage}, {@code TaskManagerStorage}and {@code UserPrefStorage}.
*/
- public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) {
+ public StorageManager(AddressBookStorage addressBookStorage,
+ CalendarStorage calendarStorage,
+ TaskManagerStorage taskManagerStorage,
+ UserPrefsStorage userPrefsStorage) {
this.addressBookStorage = addressBookStorage;
+ this.calendarStorage = calendarStorage;
+ this.taskManagerStorage = taskManagerStorage;
this.userPrefsStorage = userPrefsStorage;
}
@@ -45,7 +55,6 @@ public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException {
userPrefsStorage.saveUserPrefs(userPrefs);
}
-
// ================ AddressBook methods ==============================
@Override
@@ -75,4 +84,60 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro
addressBookStorage.saveAddressBook(addressBook, filePath);
}
+ // ================ Calendar methods =================================
+ @Override
+ public Path getCalendarFilePath() {
+ return calendarStorage.getCalendarFilePath();
+ }
+
+ @Override
+ public Optional readCalendar() throws DataLoadingException {
+ return readCalendar(calendarStorage.getCalendarFilePath());
+ }
+
+ @Override
+ public Optional readCalendar(Path filePath) throws DataLoadingException {
+ logger.fine("Attempting to read data from file: " + filePath);
+ return calendarStorage.readCalendar(filePath);
+ }
+
+ @Override
+ public void saveCalendar(ReadOnlyCalendar calendar) throws IOException {
+ saveCalendar(calendar, calendarStorage.getCalendarFilePath());
+ }
+
+ @Override
+ public void saveCalendar(ReadOnlyCalendar calendar, Path filePath) throws IOException {
+ logger.fine("Attempting to write to data file: " + filePath);
+ calendarStorage.saveCalendar(calendar, filePath);
+ }
+
+ // ================ TaskManager methods =================================
+ @Override
+ public Path getTaskManagerFilePath() {
+ return taskManagerStorage.getTaskManagerFilePath();
+ }
+
+ @Override
+ public Optional readTaskManager() throws DataLoadingException {
+ return readTaskManager(taskManagerStorage.getTaskManagerFilePath());
+ }
+
+ @Override
+ public Optional readTaskManager(Path filePath) throws DataLoadingException {
+ logger.fine("Attempting to read data from file: " + filePath);
+ return taskManagerStorage.readTaskManager(filePath);
+ }
+
+ @Override
+ public void saveTaskManager(ReadOnlyTaskManager taskManager) throws IOException {
+ saveTaskManager(taskManager, taskManagerStorage.getTaskManagerFilePath());
+ }
+
+ @Override
+ public void saveTaskManager(ReadOnlyTaskManager taskManager, Path filePath) throws IOException {
+ logger.fine("Attempting to write to data file: " + filePath);
+ taskManagerStorage.saveTaskManager(taskManager, filePath);
+ }
+
}
diff --git a/src/main/java/seedu/address/storage/TaskManagerStorage.java b/src/main/java/seedu/address/storage/TaskManagerStorage.java
new file mode 100644
index 00000000000..1417be8ae20
--- /dev/null
+++ b/src/main/java/seedu/address/storage/TaskManagerStorage.java
@@ -0,0 +1,45 @@
+package seedu.address.storage;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+
+import seedu.address.commons.exceptions.DataLoadingException;
+import seedu.address.model.task.ReadOnlyTaskManager;
+
+/**
+ * Represents a storage for {@link seedu.address.model.task.TaskManager}.
+ */
+public interface TaskManagerStorage {
+
+ /**
+ * Returns the file path of the data file.
+ */
+ Path getTaskManagerFilePath();
+
+ /**
+ * Returns TaskManager data as a {@link ReadOnlyTaskManager}.
+ * Returns {@code Optional.empty()} if storage file is not found.
+ *
+ * @throws DataLoadingException if loading the data from storage failed.
+ */
+ Optional readTaskManager() throws DataLoadingException;
+
+ /**
+ * @see #getTaskManagerFilePath()
+ */
+ Optional readTaskManager(Path filePath) throws DataLoadingException;
+
+ /**
+ * Saves the given {@link ReadOnlyTaskManager} to the storage.
+ * @param taskManager cannot be null.
+ * @throws IOException if there was any problem writing to the file.
+ */
+ void saveTaskManager(ReadOnlyTaskManager taskManager) throws IOException;
+
+ /**
+ * @see #saveTaskManager(ReadOnlyTaskManager)
+ */
+ void saveTaskManager(ReadOnlyTaskManager taskManager, Path filePath) throws IOException;
+
+}
diff --git a/src/main/java/seedu/address/ui/BottomListPanel.java b/src/main/java/seedu/address/ui/BottomListPanel.java
new file mode 100644
index 00000000000..7f7f686a441
--- /dev/null
+++ b/src/main/java/seedu/address/ui/BottomListPanel.java
@@ -0,0 +1,15 @@
+package seedu.address.ui;
+
+import javafx.scene.layout.Region;
+
+/**
+ * A panel class for list panel classes that should appear on the lower list.
+ */
+abstract class BottomListPanel extends UiPart {
+ /**
+ * Creates a {@code BottomListPanel} with the given fxmlFileName.
+ */
+ public BottomListPanel(String fxmlFileName) {
+ super(fxmlFileName);
+ }
+}
diff --git a/src/main/java/seedu/address/ui/CalendarContainer.java b/src/main/java/seedu/address/ui/CalendarContainer.java
new file mode 100644
index 00000000000..b7113af29fd
--- /dev/null
+++ b/src/main/java/seedu/address/ui/CalendarContainer.java
@@ -0,0 +1,102 @@
+package seedu.address.ui;
+
+import static seedu.address.ui.UiConstants.POPUP_CALENDAR_HEIGHT;
+import static seedu.address.ui.UiConstants.POPUP_CALENDAR_WIDTH;
+
+import javafx.fxml.FXML;
+import javafx.scene.Scene;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+
+/**
+ * Represents the container for displaying the calendar view in the user interface.
+ * It contains various graphical elements such as the calendar label, event space, and background.
+ */
+public class CalendarContainer extends UiPart {
+ private static final String FXML = "CalendarContainer.fxml";
+
+ private ReadOnlyCalendar calendar;
+ @FXML
+ private StackPane calendarLabelPlaceholder;
+
+ @FXML
+ private StackPane eventSpaceContainer;
+
+ @FXML
+ private GridPane eventSpaceBackground;
+ @FXML
+ private GridPane eventSpace;
+
+ /**
+ * Constructs a CalendarContainer with the specified ReadOnlyCalendar.
+ *
+ * @param calendar The ReadOnlyCalendar to be displayed in the container.
+ */
+ public CalendarContainer(ReadOnlyCalendar calendar) {
+ super(FXML);
+ this.calendar = calendar;
+ }
+
+ /**
+ * Creates and returns a default CalendarContainer for the specified calendar, filling it with the default
+ * components.
+ *
+ * @param calendar The ReadOnlyCalendar to be displayed.
+ * @return The default CalendarContainer.
+ */
+ public static CalendarContainer createDefaultCalendar(ReadOnlyCalendar calendar) {
+ CalendarContainer defaultCalendarContainer = new CalendarContainer(calendar);
+ defaultCalendarContainer.fillCalendar();
+ return defaultCalendarContainer;
+ }
+
+ /**
+ * Creates and returns a CalendarContainer for the specified comparison calendar, filling it solid event cards.
+ *
+ * @param calendar The ReadOnlyCalendar generated after comparison.
+ * @return The CalendarContainer for the resultant comparison calendar.
+ */
+ private static CalendarContainer createComparisonCalendar(ReadOnlyCalendar calendar) {
+ CalendarContainer comparisonCalendarContainer = new CalendarContainer(calendar);
+ comparisonCalendarContainer.fillComparisonCalendar();
+ return comparisonCalendarContainer;
+ }
+
+ /**
+ * Displays a comparison calendar in a separate modal window.
+ *
+ * @param calendar The ReadOnlyCalendar generated after comparison.
+ */
+ public static void displayComparisonCalendar(ReadOnlyCalendar calendar) {
+ Stage comparisonCalendarStage = new Stage();
+ comparisonCalendarStage.setResizable(false);
+ comparisonCalendarStage.initModality(Modality.APPLICATION_MODAL);
+ comparisonCalendarStage.setMinHeight(POPUP_CALENDAR_HEIGHT);
+ comparisonCalendarStage.setMinWidth(POPUP_CALENDAR_WIDTH);
+ CalendarContainer root = CalendarContainer.createComparisonCalendar(calendar);
+ comparisonCalendarStage.setScene(new Scene(root.getRoot()));
+ comparisonCalendarStage.show();
+ }
+
+ /**
+ * Fills the calendar container with the necessary components (calendar labels and event spaces).
+ */
+ public void fillCalendar() {
+ calendarLabelPlaceholder.getChildren().add(new CalendarLabelColumn().getRoot());
+ eventSpaceBackground.getChildren().add(new EventSpaceBackground(calendar).getRoot());
+ eventSpace.getChildren().add(CalendarEventSpace.createDefaultEventSpace(calendar).getRoot());
+ }
+
+ /**
+ * Fills the calendar container with the necessary components for the comparison calendar view.
+ */
+ public void fillComparisonCalendar() {
+ calendarLabelPlaceholder.getChildren().add(new CalendarLabelColumn().getRoot());
+ eventSpaceBackground.getChildren().add(new EventSpaceBackground(calendar).getRoot());
+ eventSpace.getChildren().add(CalendarEventSpace.createComparisonCalendarEventSpace(calendar).getRoot());
+ }
+}
diff --git a/src/main/java/seedu/address/ui/CalendarEventSpace.java b/src/main/java/seedu/address/ui/CalendarEventSpace.java
new file mode 100644
index 00000000000..123afdbc1d3
--- /dev/null
+++ b/src/main/java/seedu/address/ui/CalendarEventSpace.java
@@ -0,0 +1,261 @@
+package seedu.address.ui;
+
+import static java.time.temporal.ChronoUnit.MINUTES;
+
+import java.time.LocalTime;
+import java.util.stream.Stream;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.StrokeType;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.event.Event;
+
+/**
+ * The UI component that represents the space for displaying events in a calendar view.
+ */
+public class CalendarEventSpace extends UiPart {
+ private static final String FXML = "CalendarEventSpace.fxml";
+ private static final LocalTime DEFAULT_CALENDAR_START_TIME = LocalTime.of(8, 0);
+ private static final LocalTime DEFAULT_CALENDAR_END_TIME = LocalTime.of(18, 0);
+ private static final int NUMBER_OF_ROWS = 8;
+ private static final int NUMBER_OF_HALF_HOURS_IN_HOUR = 2;
+ private static final int NUMBER_OF_MINUTES_IN_HALF_HOUR = 30;
+ private static final int NUMBER_OF_MINUTES_IN_AN_HOUR = 60;
+ private static final int NODE_WIDTH_PER_HALF_HOUR = 25;
+ private static final int MAXIMUM_DISPLAY_HOUR_OF_DAY = 23;
+ private static final int NODE_HEIGHT = 30;
+
+ private final ReadOnlyCalendar calendar;
+ private final ObservableList eventList;
+ private final ObservableList rowList = FXCollections.observableArrayList();
+ private LocalTime calendarStartTime = DEFAULT_CALENDAR_START_TIME;
+ private LocalTime calendarEndTime = DEFAULT_CALENDAR_END_TIME;
+
+ @FXML
+ private GridPane eventSpace;
+
+ /**
+ * Constructs a CalendarEventSpace with the given ReadOnlyCalendar.
+ *
+ * @param calendar The ReadOnlyCalendar to be displayed in the event space.
+ */
+ public CalendarEventSpace(ReadOnlyCalendar calendar) {
+ super(FXML);
+ this.calendar = calendar;
+ eventList = calendar.getCurrentWeekEventList();
+ }
+
+ /**
+ * Creates the default EventSpace layout for calendar UI.
+ *
+ * @param calendar ReadOnlyCalendar to be displayed in the event space.
+ * @return CalendarEventSpace object with default layout.
+ */
+ public static CalendarEventSpace createDefaultEventSpace(ReadOnlyCalendar calendar) {
+ CalendarEventSpace defaultEventSpace = new CalendarEventSpace(calendar);
+ defaultEventSpace.updateStartAndEnd();
+ defaultEventSpace.initializeEventSpace();
+ defaultEventSpace.addEventCards();
+ defaultEventSpace.addListenerToEventList();
+ return defaultEventSpace;
+ }
+
+ /**
+ * Creates the EventSpace layout for comparison calendar UI.
+ *
+ * @param calendar ReadOnlyCalendar generated from comparison to be displayed in the event space.
+ * @return CalendarEventSpace object with comparison layout.
+ */
+ public static CalendarEventSpace createComparisonCalendarEventSpace(ReadOnlyCalendar calendar) {
+ CalendarEventSpace comparisonEventSpace = new CalendarEventSpace(calendar);
+ comparisonEventSpace.updateStartAndEnd();
+ comparisonEventSpace.initializeEventSpace();
+ comparisonEventSpace.addSolidEventCards();
+ comparisonEventSpace.addListenerToComparisonCalendarEventList();
+ return comparisonEventSpace;
+ }
+
+ /**
+ * Initializes the event space by creating and adding empty rows to the grid.
+ */
+ private void initializeEventSpace() {
+ Stream.generate(AnchorPane::new).limit(NUMBER_OF_ROWS).forEachOrdered(pane -> {
+ pane.setPrefHeight(NODE_HEIGHT);
+ pane.setPrefWidth(calculateCalendarWidth());
+ pane.setMaxWidth(calculateCalendarWidth());
+ eventSpace.addRow(eventSpace.getRowCount(), pane);
+ rowList.add(pane);
+ });
+ }
+
+ /**
+ * Adds a listener to the event list to update the event space when events change.
+ */
+ private void addListenerToEventList() {
+ eventList.addListener((ListChangeListener) c -> {
+ clear();
+ updateStartAndEnd();
+ initializeEventSpace();
+ addEventCards();
+ });
+ }
+
+ /**
+ * Adds a listener to the event list for the comparison calendar to update the event space when events change.
+ */
+ private void addListenerToComparisonCalendarEventList() {
+ eventList.addListener((ListChangeListener) c -> {
+ clear();
+ updateStartAndEnd();
+ initializeEventSpace();
+ addSolidEventCards();
+ });
+ }
+
+ /**
+ * Clears the event space by removing all event cards.
+ */
+ private void clear() {
+ eventSpace.getChildren().clear();
+ rowList.clear();
+ }
+
+ /**
+ * Calculates the width of the calendar space based on the time range being displayed.
+ *
+ * @return The calculated width of the calendar space.
+ */
+ private int calculateCalendarWidth() {
+ long minutesBetweenStartAndEnd = MINUTES.between(calendarStartTime, calendarEndTime);
+ int numberOfHoursToShow = calendarEndTime.getHour() - calendarStartTime.getHour();
+ if (minutesBetweenStartAndEnd % NUMBER_OF_MINUTES_IN_AN_HOUR > 0) {
+ numberOfHoursToShow++;
+ }
+ return numberOfHoursToShow * NODE_WIDTH_PER_HALF_HOUR * NUMBER_OF_HALF_HOURS_IN_HOUR;
+ }
+
+
+ /**
+ * Updates the start and end times for the displayed calendar.
+ */
+ private void updateStartAndEnd() {
+ LocalTime newStartTime = calendar.getEarliestEventStartTimeInCurrentWeek()
+ .map(time -> {
+ return time.minusMinutes(time.getMinute());
+ })
+ .orElse(DEFAULT_CALENDAR_START_TIME);
+ LocalTime newEndTime = calendar.getLatestEventEndTimeInCurrentWeek()
+ .map(time -> {
+ if (time.getMinute() == 0 || time.getHour() == MAXIMUM_DISPLAY_HOUR_OF_DAY) {
+ return time;
+ }
+ return time.plusMinutes(NUMBER_OF_MINUTES_IN_AN_HOUR - time.getMinute());
+ })
+ .orElse(DEFAULT_CALENDAR_END_TIME);
+
+ calendarStartTime = newStartTime.isBefore(DEFAULT_CALENDAR_START_TIME)
+ ? newStartTime : DEFAULT_CALENDAR_START_TIME;
+ calendarEndTime = newEndTime.isAfter(DEFAULT_CALENDAR_END_TIME)
+ ? newEndTime : DEFAULT_CALENDAR_END_TIME;
+ }
+
+ /**
+ * Adds event cards to the event space for all events.
+ */
+ private void addEventCards() {
+ eventList.forEach(event -> {
+ StackPane eventNode = generateEventCard(event);
+ rowList.get(findRow(event)).getChildren().add(eventNode);
+ AnchorPane.setLeftAnchor(eventNode, findLeftOffset(event));
+ });
+ }
+
+ /**
+ * Adds solid event cards to the event space for comparison calendar.
+ */
+ private void addSolidEventCards() {
+ eventList.forEach(event -> {
+ StackPane eventNode = generateSolidEventCard(event);
+ rowList.get(findRow(event)).getChildren().add(eventNode);
+ AnchorPane.setLeftAnchor(eventNode, findLeftOffset(event));
+ });
+ }
+
+ /**
+ * Generates an event card for the given event.
+ *
+ * @param event The event for which to create a card.
+ * @return A StackPane containing the event card.
+ */
+ private StackPane generateEventCard(Event event) {
+ double widthMultiplier = (double) event.getDurationOfEvent().toMinutes() / NUMBER_OF_MINUTES_IN_AN_HOUR;
+ StackPane cardHolder = new StackPane();
+
+ Rectangle cardRectangle = new Rectangle();
+ cardRectangle.setHeight(NODE_HEIGHT);
+ cardRectangle.setWidth(widthMultiplier * NODE_WIDTH_PER_HALF_HOUR * NUMBER_OF_HALF_HOURS_IN_HOUR);
+ cardRectangle.setFill(Color.CRIMSON);
+ cardRectangle.setStroke(Color.BLACK);
+ cardRectangle.setStrokeType(StrokeType.INSIDE);
+
+
+ Label description = new Label();
+ description.setMaxHeight(NODE_HEIGHT);
+ description.setMaxWidth(widthMultiplier * NODE_WIDTH_PER_HALF_HOUR * NUMBER_OF_HALF_HOURS_IN_HOUR);
+ description.setText(event.getDescriptionString());
+ description.setAlignment(Pos.CENTER);
+
+ cardHolder.getChildren().addAll(cardRectangle, description);
+ return cardHolder;
+ }
+
+ /**
+ * Generates a solid event card (no description and single color) for the given event.
+ *
+ * @param event The event for which to create a card.
+ * @return A StackPane containing the solid event card.
+ */
+ private StackPane generateSolidEventCard(Event event) {
+ double widthMultiplier = (double) event.getDurationOfEvent().toMinutes() / NUMBER_OF_MINUTES_IN_AN_HOUR;
+ StackPane cardHolder = new StackPane();
+ Rectangle cardRectangle = new Rectangle();
+ cardRectangle.setHeight(NODE_HEIGHT);
+ cardRectangle.setWidth(widthMultiplier * NODE_WIDTH_PER_HALF_HOUR * NUMBER_OF_HALF_HOURS_IN_HOUR);
+ cardRectangle.setFill(Color.GREY);
+ cardHolder.getChildren().add(cardRectangle);
+ return cardHolder;
+ }
+
+ /**
+ * Determines the row (day) in which an event should be placed in the event space.
+ *
+ * @param event The event for which to find the row.
+ * @return The row (day) index.
+ */
+ private int findRow(Event event) {
+ return event.getDayOfWeek().getValue();
+ }
+
+
+ /**
+ * Calculates the left offset for positioning an event card in the event space.
+ *
+ * @param event The event for which to calculate the left offset.
+ * @return The left offset in pixels.
+ */
+ private double findLeftOffset(Event event) {
+ return ((double) event.getMinutesFromTimeToStartTime(calendarStartTime)
+ / NUMBER_OF_MINUTES_IN_HALF_HOUR) * NODE_WIDTH_PER_HALF_HOUR;
+ }
+}
diff --git a/src/main/java/seedu/address/ui/CalendarLabelColumn.java b/src/main/java/seedu/address/ui/CalendarLabelColumn.java
new file mode 100644
index 00000000000..689274e824d
--- /dev/null
+++ b/src/main/java/seedu/address/ui/CalendarLabelColumn.java
@@ -0,0 +1,17 @@
+package seedu.address.ui;
+
+import javafx.scene.layout.Region;
+
+/**
+ * The UI component that represents the column of labels in a calendar view, typically displaying day names or headers.
+ */
+public class CalendarLabelColumn extends UiPart {
+ private static final String FXML = "CalendarLabelColumn.fxml";
+
+ /**
+ * Constructs a CalendarLabelColumn with the appropriate FXML file.
+ */
+ public CalendarLabelColumn() {
+ super(FXML);
+ }
+}
diff --git a/src/main/java/seedu/address/ui/EventCard.java b/src/main/java/seedu/address/ui/EventCard.java
new file mode 100644
index 00000000000..adb81ea90ab
--- /dev/null
+++ b/src/main/java/seedu/address/ui/EventCard.java
@@ -0,0 +1,45 @@
+package seedu.address.ui;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import seedu.address.model.event.Event;
+
+/**
+ * A UI component that displays information of a {@code Event}.
+ */
+public class EventCard extends UiPart {
+
+ 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 description;
+ @FXML
+ private Label id;
+ @FXML
+ private Label eventPeriod;
+
+ /**
+ * Creates a {@code EventCard} with the given {@code Event} and index to display.
+ */
+ public EventCard(Event event, int displayedIndex) {
+ super(FXML);
+ this.event = event;
+ id.setText(displayedIndex + ". ");
+ description.setText(event.getDescription().getDescription());
+ eventPeriod.setText(event.getEventPeriod().getFormattedPeriod());
+ }
+}
diff --git a/src/main/java/seedu/address/ui/EventListContainer.java b/src/main/java/seedu/address/ui/EventListContainer.java
new file mode 100644
index 00000000000..9ce8d86b476
--- /dev/null
+++ b/src/main/java/seedu/address/ui/EventListContainer.java
@@ -0,0 +1,35 @@
+package seedu.address.ui;
+
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import seedu.address.model.event.Event;
+
+/**
+ * A pop-up window that shows an event list of a specific person in the addressbook.
+ */
+public class EventListContainer extends UiPart {
+ private static final String FXML = "EventListContainer.fxml";
+
+ @FXML
+ private StackPane eventListPlaceholder;
+
+ private final EventListPanel eventListPanel;
+
+ /**
+ * Constructs an EventListContainer with the given event list
+ */
+ public EventListContainer(ObservableList eventList) {
+ super(FXML);
+ this.eventListPanel = new EventListPanel(eventList);
+ fillInnerPart();
+ }
+
+ /**
+ * Fills up the placeholder of this window
+ */
+ void fillInnerPart() {
+ eventListPlaceholder.getChildren().add(eventListPanel.getRoot());
+ }
+}
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..5cea83c7b2b
--- /dev/null
+++ b/src/main/java/seedu/address/ui/EventListPanel.java
@@ -0,0 +1,48 @@
+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 seedu.address.commons.core.LogsCenter;
+import seedu.address.model.event.Event;
+
+/**
+ * Panel containing the list of events.
+ */
+public class EventListPanel extends BottomListPanel {
+ private static final String FXML = "EventListPanel.fxml";
+ private final Logger logger = LogsCenter.getLogger(EventListPanel.class);
+
+ @FXML
+ private ListView eventListView;
+
+ /**
+ * Creates a {@code EventListPanel} with the given {@code ObservableList}.
+ */
+ public EventListPanel(ObservableList eventList) {
+ super(FXML);
+ 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 {
+ setGraphic(new EventCard(event, getIndex() + 1).getRoot());
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/address/ui/EventSpaceBackground.java b/src/main/java/seedu/address/ui/EventSpaceBackground.java
new file mode 100644
index 00000000000..58aab043ab7
--- /dev/null
+++ b/src/main/java/seedu/address/ui/EventSpaceBackground.java
@@ -0,0 +1,114 @@
+package seedu.address.ui;
+
+import java.time.LocalTime;
+import java.util.stream.Stream;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import seedu.address.model.calendar.ReadOnlyCalendar;
+import seedu.address.model.event.Event;
+
+/**
+ * The UI component that represents the background grid for an event space, displaying time intervals.
+ */
+public class EventSpaceBackground extends UiPart {
+ private static final String FXML = "EventSpaceBackground.fxml";
+ private static final LocalTime DEFAULT_CALENDAR_START_TIME = LocalTime.of(8, 0);
+ private static final LocalTime DEFAULT_CALENDAR_END_TIME = LocalTime.of(18, 0);
+ private static final int NUMBER_OF_HOURS_IN_ONE_DAY = 24;
+ private static final int MAXIMUM_DISPLAY_HOUR = 23;
+ private static final int NUMBER_OF_MINUTES_IN_AN_HOUR = 60;
+
+ private LocalTime calendarStartTime = DEFAULT_CALENDAR_START_TIME;
+ private LocalTime calendarEndTime = DEFAULT_CALENDAR_END_TIME;
+
+ private ReadOnlyCalendar calendar;
+ private ObservableList eventList;
+ private ObservableList columns;
+
+ @FXML
+ private HBox backgroundGrid;
+
+ /**
+ * Constructs an EventSpaceBackground with the given ReadOnlyCalendar.
+ *
+ * @param calendar The ReadOnlyCalendar to be displayed in the background.
+ */
+ public EventSpaceBackground(ReadOnlyCalendar calendar) {
+ super(FXML);
+ this.calendar = calendar;
+ eventList = calendar.getCurrentWeekEventList();
+ columns = FXCollections.observableArrayList();
+
+ fillBackground();
+ updateStartAndEnd();
+ showRelevantBackground();
+ addListenerToEventList();
+ }
+
+
+ /**
+ * Adds a listener to the event list to update the background when events change.
+ */
+ private void addListenerToEventList() {
+ eventList.addListener((ListChangeListener) c -> {
+ updateStartAndEnd();
+ showRelevantBackground();
+ });
+ }
+
+ /**
+ * Fills the background with time intervals.
+ */
+ public void fillBackground() {
+ Stream.iterate(LocalTime.MIDNIGHT, time -> time.plusHours(1))
+ .limit(NUMBER_OF_HOURS_IN_ONE_DAY)
+ .map(EventSpaceBackgroundColumn::new)
+ .forEachOrdered(column -> columns.add(column));
+ columns.forEach(column -> backgroundGrid.getChildren().add(column.getRoot()));
+ }
+
+ /**
+ * Updates the start and end times based on the events in the calendar.
+ */
+ private void updateStartAndEnd() {
+ LocalTime newStartTime = calendar.getEarliestEventStartTimeInCurrentWeek()
+ .map(time -> {
+ return time.minusMinutes(time.getMinute());
+ })
+ .orElse(DEFAULT_CALENDAR_START_TIME);
+ LocalTime newEndTime = calendar.getLatestEventEndTimeInCurrentWeek()
+ .map(time -> {
+ if (time.getMinute() == 0 || time.getHour() == MAXIMUM_DISPLAY_HOUR) {
+ return time;
+ }
+ return time.plusMinutes(NUMBER_OF_MINUTES_IN_AN_HOUR - time.getMinute());
+ })
+ .orElse(DEFAULT_CALENDAR_END_TIME);
+
+ calendarStartTime = newStartTime.isBefore(DEFAULT_CALENDAR_START_TIME)
+ ? newStartTime : DEFAULT_CALENDAR_START_TIME;
+ calendarEndTime = newEndTime.isAfter(DEFAULT_CALENDAR_END_TIME)
+ ? newEndTime : DEFAULT_CALENDAR_END_TIME;
+ }
+
+ /**
+ * Shows the relevant background columns based on the current time range.
+ */
+ private void showRelevantBackground() {
+ columns.stream().filter(column -> !column.isWithin(calendarStartTime, calendarEndTime))
+ .map(EventSpaceBackgroundColumn::getRoot).forEach(region -> {
+ region.setVisible(false);
+ region.setManaged(false);
+ });
+ columns.stream().filter(column -> column.isWithin(calendarStartTime, calendarEndTime))
+ .map(EventSpaceBackgroundColumn::getRoot).forEach(region -> {
+ region.setVisible(true);
+ region.setManaged(true);
+ });
+ }
+}
diff --git a/src/main/java/seedu/address/ui/EventSpaceBackgroundColumn.java b/src/main/java/seedu/address/ui/EventSpaceBackgroundColumn.java
new file mode 100644
index 00000000000..31e73f63871
--- /dev/null
+++ b/src/main/java/seedu/address/ui/EventSpaceBackgroundColumn.java
@@ -0,0 +1,123 @@
+package seedu.address.ui;
+
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.stream.Stream;
+
+import javafx.fxml.FXML;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.scene.shape.Line;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.StrokeType;
+
+/**
+ * The UI component that represents a column in the event space background, displaying time intervals.
+ */
+public class EventSpaceBackgroundColumn extends UiPart {
+ private static final String FXML = "EventSpaceBackgroundColumn.fxml";
+ private static final String DIVIDER_LINE_STROKE_COLOUR_STRING = "#86826a";
+ private static final String RECTANGLE_STROKE_COLOUR_STRING = "#dbc3ae";
+ private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
+ private static final int NUMBER_OF_DAY_CELLS = 7;
+ private static final int RECTANGLE_ARC_VALUE = 5;
+ private static final int CELL_HEIGHT = 30;
+ private static final int CELL_WIDTH = 50;
+ private static final int RECTANGLE_WIDTH = 25;
+ private static final int DIVIDER_LINE_LENGTH = 28;
+ private static final int DIVIDER_LINE_STROKE_WIDTH = 2;
+ private static final double DIVIDER_LINE_OPACITY = 0.5;
+ private final LocalTime startTime;
+
+ @FXML
+ private VBox columnContainer;
+
+ @FXML
+ private Label startTimeLabel;
+
+ /**
+ * Constructs an EventSpaceBackgroundColumn with the specified start time.
+ *
+ * @param startTime The start time represented by this column.
+ */
+ public EventSpaceBackgroundColumn(LocalTime startTime) {
+ super(FXML);
+ this.startTime = startTime;
+
+ setLabel();
+ setColumnRectangles();
+ }
+
+ /**
+ * Checks if this column's time interval is within the specified time range.
+ *
+ * @param start The start time of the range (inclusive).
+ * @param end The end time of the range (exclusive).
+ * @return True if the column's time interval is within the specified range; otherwise, false.
+ */
+ public boolean isWithin(LocalTime start, LocalTime end) {
+ return (startTime.equals(start) || (startTime.isAfter(start) && startTime.isBefore(end)))
+ && !startTime.equals(end);
+ }
+
+ /**
+ * Sets the label to display the start time formatted as "HH:mm".
+ */
+ private void setLabel() {
+ startTimeLabel.setText(startTime.format(TIME_FORMATTER));
+ }
+
+ /**
+ * Sets up the rectangles and divider line for each day cell in the column.
+ */
+ private void setColumnRectangles() {
+ Stream.generate(this::constructDayColumnCell).limit(NUMBER_OF_DAY_CELLS)
+ .forEachOrdered(cell -> columnContainer.getChildren().add(cell));
+ }
+
+ /**
+ * Constructs a day column cell with rectangles and a divider line.
+ *
+ * @return StackPane representing a day column cell.
+ */
+ private StackPane constructDayColumnCell() {
+ StackPane cellContainer = new StackPane();
+ VBox.setVgrow(cellContainer, Priority.NEVER);
+ HBox.setHgrow(cellContainer, Priority.NEVER);
+ cellContainer.setPrefHeight(CELL_HEIGHT);
+ cellContainer.setPrefWidth(CELL_WIDTH);
+ HBox rectangleContainer = new HBox();
+ Rectangle leftRectangle = new Rectangle(RECTANGLE_WIDTH, CELL_HEIGHT);
+ Rectangle rightRectangle = new Rectangle(RECTANGLE_WIDTH, CELL_HEIGHT);
+ Line dividerLine = new Line();
+
+ leftRectangle.setFill(Color.ANTIQUEWHITE);
+ rightRectangle.setFill(Color.BISQUE);
+ leftRectangle.setArcHeight(RECTANGLE_ARC_VALUE);
+ leftRectangle.setArcWidth(RECTANGLE_ARC_VALUE);
+ rightRectangle.setArcHeight(RECTANGLE_ARC_VALUE);
+ rightRectangle.setArcWidth(RECTANGLE_ARC_VALUE);
+ leftRectangle.setStroke(Paint.valueOf(RECTANGLE_STROKE_COLOUR_STRING));
+ leftRectangle.setStrokeType(StrokeType.INSIDE);
+ rightRectangle.setStroke(Paint.valueOf(RECTANGLE_STROKE_COLOUR_STRING));
+ rightRectangle.setStrokeType(StrokeType.INSIDE);
+
+ dividerLine.setStroke(Paint.valueOf(DIVIDER_LINE_STROKE_COLOUR_STRING));
+ dividerLine.setEndY(DIVIDER_LINE_LENGTH);
+ dividerLine.setStrokeWidth(DIVIDER_LINE_STROKE_WIDTH);
+ dividerLine.setOpacity(DIVIDER_LINE_OPACITY);
+ dividerLine.setFill(Color.GAINSBORO);
+ StackPane.setAlignment(dividerLine, Pos.CENTER);
+
+ rectangleContainer.getChildren().addAll(leftRectangle, rightRectangle);
+ cellContainer.getChildren().addAll(rectangleContainer, dividerLine);
+ return cellContainer;
+ }
+}
diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java
index 3f16b2fcf26..e58576f3b33 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-cs2103-f13-4.github.io/tp/UserGuide.html#quick-start";
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..8a9c754a459 100644
--- a/src/main/java/seedu/address/ui/MainWindow.java
+++ b/src/main/java/seedu/address/ui/MainWindow.java
@@ -4,6 +4,7 @@
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
+import javafx.scene.Scene;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextInputControl;
import javafx.scene.input.KeyCombination;
@@ -12,10 +13,12 @@
import javafx.stage.Stage;
import seedu.address.commons.core.GuiSettings;
import seedu.address.commons.core.LogsCenter;
+import seedu.address.commons.core.index.Index;
import seedu.address.logic.Logic;
import seedu.address.logic.commands.CommandResult;
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.logic.parser.exceptions.ParseException;
+import seedu.address.model.person.Person;
/**
* The Main Window. Provides the basic application layout containing
@@ -24,13 +27,18 @@
public class MainWindow extends UiPart {
private static final String FXML = "MainWindow.fxml";
+ private static final int BOTTOM_LIST_MODES = 2;
+ private static final int POPUP_LIST_HEIGHT = 500;
+ private static final int POPUP_LIST_WIDTH = 555;
private final Logger logger = LogsCenter.getLogger(getClass());
private Stage primaryStage;
private Logic logic;
+ private int bottomListIndicator;
// Independent Ui parts residing in this Ui container
+ private BottomListPanel bottomListPanel;
private PersonListPanel personListPanel;
private ResultDisplay resultDisplay;
private HelpWindow helpWindow;
@@ -41,6 +49,9 @@ public class MainWindow extends UiPart {
@FXML
private MenuItem helpMenuItem;
+ @FXML
+ private StackPane bottomListPanelPlaceholder;
+
@FXML
private StackPane personListPanelPlaceholder;
@@ -50,6 +61,13 @@ public class MainWindow extends UiPart {
@FXML
private StackPane statusbarPlaceholder;
+ @FXML
+ private StackPane calendarPlaceholder;
+
+ private EventListPanel eventListPanel;
+
+ private TaskListPanel taskListPanel;
+
/**
* Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}.
*/
@@ -66,6 +84,7 @@ public MainWindow(Stage primaryStage, Logic logic) {
setAccelerators();
helpWindow = new HelpWindow();
+ bottomListIndicator = 0;
}
public Stage getPrimaryStage() {
@@ -110,6 +129,12 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) {
* Fills up all the placeholders of this window.
*/
void fillInnerParts() {
+ eventListPanel = new EventListPanel(logic.getEventList());
+ taskListPanel = new TaskListPanel(logic.getTaskList());
+
+ bottomListPanel = eventListPanel;
+ bottomListPanelPlaceholder.getChildren().add(bottomListPanel.getRoot());
+
personListPanel = new PersonListPanel(logic.getFilteredPersonList());
personListPanelPlaceholder.getChildren().add(personListPanel.getRoot());
@@ -121,6 +146,8 @@ void fillInnerParts() {
CommandBox commandBox = new CommandBox(this::executeCommand);
commandBoxPlaceholder.getChildren().add(commandBox.getRoot());
+
+ calendarPlaceholder.getChildren().add(CalendarContainer.createDefaultCalendar(logic.getCalendar()).getRoot());
}
/**
@@ -163,6 +190,35 @@ private void handleExit() {
primaryStage.hide();
}
+ private void handleComparison() {
+ CalendarContainer.displayComparisonCalendar(logic.getComparisonCalendar());
+ }
+
+ /**
+ * Switches the bottom list between the available lists.
+ */
+ @FXML
+ private void handleSwitchBottomList() {
+ if (bottomListIndicator == 0) {
+ switchToTaskList();
+ } else {
+ switchToEventList();
+ }
+ bottomListIndicator = (bottomListIndicator + 1) % BOTTOM_LIST_MODES;
+ }
+
+ private void handleViewEventList(Index index) {
+ Person person = logic.getFilteredPersonList().get(index.getZeroBased());
+ Stage eventListStage = new Stage();
+ eventListStage.setResizable(false);
+ eventListStage.setTitle(person.getName().toString() + "'s Event List");
+ eventListStage.setMinHeight(POPUP_LIST_HEIGHT);
+ eventListStage.setMinWidth(POPUP_LIST_WIDTH);
+ EventListContainer root = new EventListContainer(person.getEventList());
+ eventListStage.setScene(new Scene(root.getRoot()));
+ eventListStage.show();
+ }
+
public PersonListPanel getPersonListPanel() {
return personListPanel;
}
@@ -186,6 +242,18 @@ private CommandResult executeCommand(String commandText) throws CommandException
handleExit();
}
+ if (commandResult.isSwitchBottomList()) {
+ handleSwitchBottomList();
+ }
+
+ if (commandResult.isViewEvents()) {
+ handleViewEventList(commandResult.getEventViewIndex());
+ }
+
+ if (commandResult.isShowCalendarComparison()) {
+ handleComparison();
+ }
+
return commandResult;
} catch (CommandException | ParseException e) {
logger.info("An error occurred while executing command: " + commandText);
@@ -193,4 +261,20 @@ private CommandResult executeCommand(String commandText) throws CommandException
throw e;
}
}
+
+ /**
+ * Switches the Bottom List Panel to display the task list instead.
+ */
+ public void switchToTaskList() {
+ bottomListPanel = taskListPanel;
+ bottomListPanelPlaceholder.getChildren().setAll(bottomListPanel.getRoot());
+ }
+
+ /**
+ * Switches the Bottom List Panel to display the event list instead.
+ */
+ public void switchToEventList() {
+ bottomListPanel = eventListPanel;
+ bottomListPanelPlaceholder.getChildren().setAll(bottomListPanel.getRoot());
+ }
}
diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java
index 094c42cda82..a63b48fdcb9 100644
--- a/src/main/java/seedu/address/ui/PersonCard.java
+++ b/src/main/java/seedu/address/ui/PersonCard.java
@@ -1,12 +1,20 @@
package seedu.address.ui;
+import static seedu.address.ui.UiConstants.POPUP_CALENDAR_HEIGHT;
+import static seedu.address.ui.UiConstants.POPUP_CALENDAR_WIDTH;
+
import java.util.Comparator;
+import javafx.event.EventHandler;
import javafx.fxml.FXML;
+import javafx.scene.Scene;
import javafx.scene.control.Label;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
+import javafx.stage.Stage;
import seedu.address.model.person.Person;
/**
@@ -15,6 +23,8 @@
public class PersonCard extends UiPart {
private static final String FXML = "PersonListCard.fxml";
+ private static final String TITLE_STRING_AFTER_NAME = "'s Calendar";
+ private static final int NUMBER_OF_CLICK_TO_SHOW_CALENDAR = 2;
/**
* Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX.
@@ -55,5 +65,35 @@ public PersonCard(Person person, int displayedIndex) {
person.getTags().stream()
.sorted(Comparator.comparing(tag -> tag.tagName))
.forEach(tag -> tags.getChildren().add(new Label(tag.tagName)));
+ setClickListener();
+ }
+
+ /**
+ * Adds a listener to detect when the user double-clicks the PersonCard.
+ */
+ private void setClickListener() {
+ cardPane.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler() {
+ @Override
+ public void handle(MouseEvent mouseEvent) {
+ if (mouseEvent.getButton().equals(MouseButton.PRIMARY)
+ && mouseEvent.getClickCount() == NUMBER_OF_CLICK_TO_SHOW_CALENDAR) {
+ showCalendar();
+ }
+ }
+ });
+ }
+
+ /**
+ * Display this person's calendar
+ */
+ private void showCalendar() {
+ Stage calendarStage = new Stage();
+ calendarStage.setResizable(false);
+ calendarStage.setTitle(person.getName().toString() + TITLE_STRING_AFTER_NAME);
+ calendarStage.setMinHeight(POPUP_CALENDAR_HEIGHT);
+ calendarStage.setMinWidth(POPUP_CALENDAR_WIDTH);
+ CalendarContainer root = CalendarContainer.createDefaultCalendar(person.getReadOnlyCalendar());
+ calendarStage.setScene(new Scene(root.getRoot()));
+ calendarStage.show();
}
}
diff --git a/src/main/java/seedu/address/ui/TaskCard.java b/src/main/java/seedu/address/ui/TaskCard.java
new file mode 100644
index 00000000000..71bdb63ae73
--- /dev/null
+++ b/src/main/java/seedu/address/ui/TaskCard.java
@@ -0,0 +1,36 @@
+package seedu.address.ui;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import seedu.address.model.task.Task;
+
+/**
+ * A UI component that displays information of a {@code Task}.
+ */
+public class TaskCard extends UiPart {
+ private static final String FXML = "TaskListCard.fxml";
+
+ public final Task task;
+
+ @FXML
+ private HBox cardPane;
+ @FXML
+ private Label description;
+ @FXML
+ private Label id;
+ @FXML
+ private Label deadline;
+
+ /**
+ * Creates a {@code TaskCard} with the given {@code Task} and index to display.
+ */
+ public TaskCard(Task task, int displayedIndex) {
+ super(FXML);
+ this.task = task;
+ id.setText(displayedIndex + ". ");
+ description.setText(task.getDescriptionString());
+ deadline.setText("Deadline: " + task.getDeadlineString());
+ }
+}
diff --git a/src/main/java/seedu/address/ui/TaskListPanel.java b/src/main/java/seedu/address/ui/TaskListPanel.java
new file mode 100644
index 00000000000..e1ee60bdc37
--- /dev/null
+++ b/src/main/java/seedu/address/ui/TaskListPanel.java
@@ -0,0 +1,47 @@
+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 seedu.address.commons.core.LogsCenter;
+import seedu.address.model.task.Task;
+
+/**
+ * Panel containing a list of tasks.
+ */
+public class TaskListPanel extends BottomListPanel {
+ private static final String FXML = "TaskListPanel.fxml";
+ private final Logger logger = LogsCenter.getLogger(TaskListPanel.class);
+
+ @FXML
+ private ListView taskListView;
+
+ /**
+ * Creates a {@code TaskListPanel} with the given {@code ObservableList}.
+ */
+ public TaskListPanel(ObservableList taskList) {
+ super(FXML);
+ taskListView.setItems(taskList);
+ taskListView.setCellFactory(listView -> new TaskListPanel.TaskListViewCell());
+ }
+
+ /**
+ * Custom {@code ListCell} that displays the graphics of a {@code Task} using a {@code TaskCard}.
+ */
+ class TaskListViewCell extends ListCell {
+ @Override
+ protected void updateItem(Task task, boolean empty) {
+ super.updateItem(task, empty);
+
+ if (empty || task == null) {
+ setGraphic(null);
+ setText(null);
+ } else {
+ setGraphic(new TaskCard(task, getIndex() + 1).getRoot());
+ }
+ }
+ }
+}
diff --git a/src/main/java/seedu/address/ui/UiConstants.java b/src/main/java/seedu/address/ui/UiConstants.java
new file mode 100644
index 00000000000..e6490e5aa33
--- /dev/null
+++ b/src/main/java/seedu/address/ui/UiConstants.java
@@ -0,0 +1,9 @@
+package seedu.address.ui;
+
+/**
+ * Stores some constants used across multiple UI classes.
+ */
+public class UiConstants {
+ public static final int POPUP_CALENDAR_HEIGHT = 285;
+ public static final int POPUP_CALENDAR_WIDTH = 555;
+}
diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java
index fdf024138bc..1871afdb6ba 100644
--- a/src/main/java/seedu/address/ui/UiManager.java
+++ b/src/main/java/seedu/address/ui/UiManager.java
@@ -18,10 +18,8 @@
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/unimate_logo.png";
private Logic logic;
private MainWindow mainWindow;
@@ -84,5 +82,4 @@ private void showFatalErrorDialogAndShutdown(String title, Throwable e) {
Platform.exit();
System.exit(1);
}
-
}
diff --git a/src/main/resources/images/unimate_logo.png b/src/main/resources/images/unimate_logo.png
new file mode 100644
index 00000000000..22b0e26ea1c
Binary files /dev/null and b/src/main/resources/images/unimate_logo.png differ
diff --git a/src/main/resources/view/CalendarContainer.fxml b/src/main/resources/view/CalendarContainer.fxml
new file mode 100644
index 00000000000..eaea0a4fc1f
--- /dev/null
+++ b/src/main/resources/view/CalendarContainer.fxml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/CalendarEventSpace.fxml b/src/main/resources/view/CalendarEventSpace.fxml
new file mode 100644
index 00000000000..39797785a6f
--- /dev/null
+++ b/src/main/resources/view/CalendarEventSpace.fxml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/CalendarLabelColumn.fxml b/src/main/resources/view/CalendarLabelColumn.fxml
new file mode 100644
index 00000000000..d080b3eda06
--- /dev/null
+++ b/src/main/resources/view/CalendarLabelColumn.fxml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css
index 36e6b001cd8..ed750843362 100644
--- a/src/main/resources/view/DarkTheme.css
+++ b/src/main/resources/view/DarkTheme.css
@@ -97,6 +97,7 @@
-fx-label-padding: 0 0 0 0;
-fx-graphic-text-gap : 0;
-fx-padding: 0 0 0 0;
+ -fx-background-color: derive(#1d1d1d, 20%);
}
.list-cell:filled:even {
@@ -120,6 +121,18 @@
-fx-text-fill: white;
}
+.calendar_header_label {
+ -fx-text-fill: "Segoe UI Semibold";
+ -fx-font-size: 18px;
+ -fx-text-fill: #c21c1c;
+ -fx-text-alignment: center;
+}
+
+.calendar_cell_border {
+ -fx-border-color: #654f4fba;
+ -fx-border-width: 1px;
+}
+
.cell_big_label {
-fx-font-family: "Segoe UI Semibold";
-fx-font-size: 16px;
@@ -142,6 +155,10 @@
-fx-border-top-width: 1px;
}
+.pane-without-border {
+ -fx-background-color: derive(#1d1d1d, 20%);
+}
+
.status-bar {
-fx-background-color: derive(#1d1d1d, 30%);
}
diff --git a/src/main/resources/view/EventListCard.fxml b/src/main/resources/view/EventListCard.fxml
new file mode 100644
index 00000000000..263ecc486f9
--- /dev/null
+++ b/src/main/resources/view/EventListCard.fxml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/EventListContainer.fxml b/src/main/resources/view/EventListContainer.fxml
new file mode 100644
index 00000000000..2ba28ecf8db
--- /dev/null
+++ b/src/main/resources/view/EventListContainer.fxml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/EventListPanel.fxml b/src/main/resources/view/EventListPanel.fxml
new file mode 100644
index 00000000000..7701eea5fa7
--- /dev/null
+++ b/src/main/resources/view/EventListPanel.fxml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/EventSpaceBackground.fxml b/src/main/resources/view/EventSpaceBackground.fxml
new file mode 100644
index 00000000000..ce6af6366c7
--- /dev/null
+++ b/src/main/resources/view/EventSpaceBackground.fxml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/main/resources/view/EventSpaceBackgroundColumn.fxml b/src/main/resources/view/EventSpaceBackgroundColumn.fxml
new file mode 100644
index 00000000000..07fad8a51a9
--- /dev/null
+++ b/src/main/resources/view/EventSpaceBackgroundColumn.fxml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml
index 7778f666a0a..e8b3b9f31d7 100644
--- a/src/main/resources/view/MainWindow.fxml
+++ b/src/main/resources/view/MainWindow.fxml
@@ -3,16 +3,18 @@
+
-
+
+
+
-
+
@@ -33,24 +35,46 @@
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+