diff --git a/README.md b/README.md index 13f5c77403f..003b4efd89b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,32 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![CI Status](https://github.com/AY2122S2-CS2103T-T12-4/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2122S2-CS2103T-T12-4/tp/actions) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. +# NUS Classes +**NUS Classes** is an app that enables the professors to better manage contacts from large numbers of students and staff, +and allows the professors to document the tasks that they need to do. + +### Features + +Features added on top of AB3. + +| Feature | Description | +|----------------|----------------------------------------------------------------------------------------------------------------------------------| +| Create Task | Add and schedule (*recurring*) tasks on NUS Classes easily with a single command. | +| Update Task | Edit tasks which are outdated or when mistakes were made. | +| Delete Task | Delete tasks which are unnecessary or when you are done with it. | +| Un/Assign Task | Assign and unassign contacts to a task. Help with organization and planning! | +| View Task | View the people assigned to a task. Help with remembering who you are suppose to meet! | +| Tag Task | Tag a task with different tags. Help with categorizing tasks! | +| Filter Task | Too many tasks? Filter them based on keywords! | +| Alert | Forgetful? No worries, tasks in NUS Classes are color coded to allow you to quickly identify tasks that are overdue or due soon! | + +### Others + +* **User Guide** for NUS Classes can be found here: [User Guide](https://ay2122s2-cs2103t-t12-4.github.io/tp/UserGuide.html). +* **Developer Guide** for NUS Classes can be found here: [Developer Guide](https://ay2122s2-cs2103t-t12-4.github.io/tp/DeveloperGuide.html). + +### Acknowledgements +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). +Additionally, this project was adopted from the existing AB3 se-education.org project. Please refer +to the AB3 product website **[here](https://se-education.org/addressbook-level3/)** for more information. diff --git a/build.gradle b/build.gradle index be2d2905dde..88545de9b64 100644 --- a/build.gradle +++ b/build.gradle @@ -16,10 +16,18 @@ repositories { maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } } +run { + enableAssertions = true +} + checkstyle { toolVersion = '8.29' } +run { + enableAssertions = true +} + test { useJUnitPlatform() finalizedBy jacocoTestReport @@ -66,7 +74,7 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'NUSClasses.jar' } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..28c773f2109 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,51 @@ title: About Us We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg). -You can reach us at the email `seer[at]comp.nus.edu.sg` +You can reach us through our Github usernames. ## Project team -### John Doe +### Brian - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/brian16600)]] [[portfolio](team/brian16600.md)] -* Role: Project Advisor +* Role: Developer +* Responsibilites: Code Quality + Documentation -### Jane Doe +### Sean Ng - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/snss231)]] [[portfolio](team/snss231.md)] -* Role: Team Lead -* Responsibilities: UI +* Role: Developer +* Responsibilities: Data -### Johnny Doe +### Ong Jun Jie - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/junjunjieong)]] [[portfolio](team/junjunjieong.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Code quality + Deliverables and deadlines -### Jean Doe +### Tan Jun Rong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/junrong98)]] [[portfolio](team/junrong98.md)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Code Quality, In Charge of Tagging -### James Doe +### Adrian Ong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/AdrianOngJJ)]] [[portfolio](team/adrianongjj.md)] * Role: Developer -* Responsibilities: UI +* Responsibilities: Glossary + Documentation diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 46eae8ee565..78837e59b99 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,14 +2,46 @@ layout: page title: Developer Guide --- -* Table of Contents -{:toc} +## Table of Contents +* [Acknowledgements](#acknowledgements) +* [Setting up, getting started](#setting-up-getting-started) +* [Design](#design) + * [Architecture](#architecture) + * [Ui component](#ui-component) + * [Logic component](#logic-component) + * [Model component](#model-component) + * [Storage component](#storage-component) + * [Common classes](#common-classes) +* [Implementation](#implementation) + * [Delete person feature](#delete-person-feature) + * [Delete task feature](#delete-task-feature) + * [Edit task feature](#edit-task-feature) + * [View task feature](#view-task-feature) + * [Find task feature](#find-task-feature) +* [Documentation, logging, testing, configuration, dev-ops](#documentation-logging-testing-configuration-dev-ops) +* [Appendix: Requirements](#appendix-requirements) + * [Product scope](#product-scope) + * [User stories](#user-stories) + * [Use cases](#use-cases) + * [Non-Functional requirements](#non-functional-requirements) +* [Appendix: Instructions for manual testing](#appendix-instructions-for-manual-testing) + * [Launch and shutdown](#launch-and-shutdown) + * [Deleting a person](#deleting-a-person) + * [Saving data](#saving-data) +* [Prefix Summary](#prefix-summary) +* [Glossary](#glossary) + +
-------------------------------------------------------------------------------------------------------------------- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* [PlantUML](https://plantuml.com/) - open-source diagramming tool used for our architecture, class, sequence and activity diagrams. +* [JUnit](https://junit.org/junit5/) - Java testing framework used for most of our testing +* [Gradle](https://gradle.org/) - Build automation tool +* [Shadowjar](https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow) - "A Gradle plugin for collapsing all dependencies and project code into a single Jar file." +* [Jackson](https://github.com/FasterXML/jackson) - "JSON for Java" library -------------------------------------------------------------------------------------------------------------------- @@ -17,15 +49,31 @@ title: Developer Guide Refer to the guide [_Setting up and getting started_](SettingUp.md). + +
+ -------------------------------------------------------------------------------------------------------------------- +## **Description** +NUS Classes is a desktop app for NUS Computing professors to manage their tasks and contacts. It includes task management features such as creating tasks, tagging tasks, assigning contacts to tasks, and marking tasks as complete or incomplete. It also includes contact management features such as finding contacts, assigning contacts to specific tasks and tagging contacts. + +NUS Classes also provides a simple alert feature for tasks by displaying tasks in different color based on the urgency of the task. Tasks that are overdue are marked as red, whereas, tasks that are nearing deadline are marked as yellow. + +NUS Classes is optimized for use via a Command Line Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). Using NUS Classes can get your contact management tasks done faster than traditional GUI apps, saving time on otherwise tedious administrative tasks. + +This Developer Guide is documented with the approach of developer-as-maintainer, explaining how the architecture and implementation of NUS Classes is done +to allow for easy maintenance and modification if necessary. In this Developer Guide, you will find explanations as well as diagrams for the main components of NUS Classes at the high-level design as well as in-depth explanations +on certain key features. + ## **Design**
-:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](https://github.com/se-edu/addressbook-level3/tree/master/docs/diagrams/) folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. +:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](diagrams/) folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams.
+
+ ### Architecture @@ -36,7 +84,7 @@ Given below is a quick overview of main components and how they interact with ea **Main components of the architecture** -**`Main`** has two classes called [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java). It is responsible for, +**`Main`** has two classes called [`Main`](https://github.com/AY2122S2-CS2103T-T12-4/tp/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2122S2-CS2103T-T12-4/tp/tree/master/src/main/java/seedu/address/MainApp.java). It is responsible for, * At app launch: Initializes the components in the correct sequence, and connects them up with each other. * At shut down: Shuts down the components and invokes cleanup methods where necessary. @@ -67,15 +115,17 @@ For example, the `Logic` component defines its API in the `Logic.java` interface The sections below give more details of each component. +
+ ### UI component -The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) +The **API** of this component is specified in [`Ui.java`](https://github.com/AY2122S2-CS2103T-T12-4/tp/tree/master/src/main/java/seedu/address/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/AY2122S2-CS2103T-T12-4/tp/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2122S2-CS2103T-T12-4/tp/tree/master/src/main/resources/view/MainWindow.fxml) The `UI` component, @@ -84,9 +134,11 @@ The `UI` component, * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. * depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +
+ ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2122S2-CS2103T-T12-4/tp/tree/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: @@ -113,8 +165,10 @@ How the parsing works: * When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. * All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. +
+ ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) +**API** : [`Model.java`](https://github.com/AY2122S2-CS2103T-T12-4/tp/tree/master/src/main/java/seedu/address/model/Model.java) @@ -122,122 +176,221 @@ How the parsing works: The `Model` component, * stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the task list data i.e., all 'Task' objects (which are contained in a `TaskList` object). +* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when some `Person`'s data in the list changes. +* stores the currently 'selected' `Task` objects (e.g. results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when some `Task`'s data in the list changes. * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
- - - -
- +
### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](https://github.com/AY2122S2-CS2103T-T12-4/tp/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, task list and user preference data in json format, and read them back into corresponding objects. +* inherits from `AddressBookStorage`, `TaskListStorage` and `UserPrefStorage`, which means it can be treated as any one of them (if only the functionality of 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 Classes used by multiple components are in the `seedu.addressbook.commons` package. +
+ + -------------------------------------------------------------------------------------------------------------------- ## **Implementation** This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### Add Task feature -#### Proposed Implementation +In NUS Classes, `Task`s represent NUS Computing professor's tasks that they have scheduled. Tasks include relevant information +such as `TASKNAME`, `DATETIME`, `LINK` for links to meetings/lectures/tutorials and `TAG`s for professors to keep their tasks organised. +When a `Task` is added to NUS Classes, the following features are implemented: +- `AddTaskCommandParser#parse()` - Parses the compulsory fields of `TASKNAME` and `DATETIME` as well as optional fields such as `ENDDATETIME`, `TAG`, `LINK` and `INTERVAL` with `REUCRRENCE`. +The parser obtains the relevant information to be added and creates a `Task` with the information parsed. +- `AddTaskCommand` which contains the relevant information for the newly added `Task`. +- `AddTaskCommand#execute()` - Execute `ModelManager#addTask()` by parsing in the task to be added and passing it to `TaskList`. +- `TaskList#addTask()` - Adds the `Task` to `TaskList`. -The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: +Step 1: User enters the command, e.g. `addt tn/Lesson dt/12-03-2022 1200`. Once this command is parsed, it is handled by `AddressBookParser#parseCommand()`, which creates a `AddTaskCommandParser` object. -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +Step 2: `AddTaskCommandParser#parse()` is then called which parses the command. The parameters entered by the user, such as `TASKNAME`, `DATETIME` and optional parameters are parsed, and a new `AddTaskCommand` object is created which contains the parameters entered by the user. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +Step 3: `AddTaskCommand` will then call `AddTaskCommand#execute()` which will execute the command. It will first create a `Task` based on the information contained and then it will add the newly created `Task` to `TaskList` via `TaskList#addTask()`. -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +Step 4: Return either a success message with the added `Task` or a `CommandException` due to missing parameters or invalid parameters. -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. +![AddTaskSequenceDiagram](images/AddTaskSequenceDiagram.png) -![UndoRedoState0](images/UndoRedoState0.png) +
:information_source: **Note:** The lifeline for `AddTaskCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
-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. +
-![UndoRedoState1](images/UndoRedoState1.png) +### Delete person feature -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`. +In NUS Classes, `Task`s are entities that maintain a list of `People` that are associated with the task. +When a contact is deleted from the `AddressBook`, it is essential that the `Task`s that contain that contact are updated to also remove the contact. +To implement this, upon every `DeleteCommand` execution, we call the `TaskList::removePerson` which iterate through all the tasks and remove the relevant `Person` from the tasks if present. -![UndoRedoState2](images/UndoRedoState2.png) + -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +Design considerations: -
+**Aspect:** how relevant tasks are updated when a person is removed from the address book + +* **Alternative 1 (current choice):** Iterate through all tasks to remove the relevant person. + * Pros: Easy to implement. + * Cons: _May_ have performance issues given a large list of tasks + +* **Alternative 2:** Add a reference from each Person to the Tasks they are associated with. When a person is deleted, reference all the tasks through the `Person` object to update the tasks. + * Pros: _May_ see some performance benefit (not necessary to iterate through all the tasks upon each `DeleteCommand`) + * Cons: More fragile code due to circular dependency (`Person` depends on `Task`). Not often that a Professor will delete a contact (student or tutor) in the course of a module. -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +
-![UndoRedoState3](images/UndoRedoState3.png) +### Delete Task feature +Delete task feature implements the following operations: +* `DeleteTaskCommandParser#parse()` — Parse the index number from user command to `DeleteTaskCommand` to get the task to be deleted. +* `DeleteTaskCommand#execute()` — Execute `ModelManager#deleteTask()` by parsing in the task to be deleted. +* `ModelManager#deleteTask()` — Execute `TaskList#deleteCurrTask()` by parsing in the task to be deleted. +* `TaskList#deleteCurrTask()` — Deletes the task from the TaskList stored here. -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather -than attempting to perform the undo. +Step 1: User will enter the command `deletet 1` to delete the first task. +Once user parses in the command, it will be handled by `AddressBookParser#parseCommand()`, then calling of `DeleteTaskCommandParser#parse()` +to create `DeleteTaskCommand` and execute to delete the task from the task list. +The Sequence Diagram below illustrates the interactions of how the delete task feature works. +![DeleteTaskSequenceDiagram](images/DeleteTaskSequenceDiagram.png) +
:information_source: **Note:** The lifeline for `DeleteTaskCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
-The following sequence diagram shows how the undo operation works: +Step 2: Outcome after executing `DeleteTaskCommand` -![UndoSequenceDiagram](images/UndoSequenceDiagram.png) +Execution flow of Activity Diagram: -
: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. +![DeleteTaskOutcomeActivityeDiagram](images/Activity Diagram/DeleteTaskOutcome.png) -
+#### Design considerations: +**Aspect:** How delete task executes: -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. +* **Alternative 1 (current choice):** Delete task based on the index shown. + * Pros: Easy to implement. + * Cons: Users have to scroll through task list to look for task index number. -
: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. +* **Alternative 2:** Delete task based on the task name. + * Pros: Users just have to enter the task name. + * Cons: Checks are needed to ensure that users entered the correct spelling and spacing of the task name -
+
+ +### Edit Task feature +Edit task feature implements the following operations: +* `EditTaskCommandParser#parse()` — Parse the command such as index of the task to edit and which information to update. +* `EditTaskCommand#execute()` — Execute `ModelManager#setTask()` by parsing in the task to be edited and the updated version of the task. +* `EditTaskDescriptor#setName()` — Set the edited task name to `EditTaskDescriptor` +* `EditTaskDescriptor#setDate()` — Set the edited datetime to `EditTaskDescriptor` +* `EditTaskDescriptor#setTags()` — Set the edited tags to `EditTaskDescriptor` +* `ParseUtil#parseIndex()` —  Parse to get the index number of the task +* `ModelManager#setTask()` — Update the task information. +* `ModelManager#updateFilteredTaskList()` — Updates the filter of the filtered task list to filter by the given predicate. -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 1: User parses in command. For example, `editt 1 tn/Teach CS2103T dt/12-03-2022 1330 z/https://zoomlink.com t/Homework` +Once user parses in the command, it will be handled by `AddressBookParser#parseCommand()`, then calling of `EditTaskCommandParser#parse()` +![EditTaskSequenceDiagramstate0](images/EditTaskDiagram/EditTaskSequenceDiagramState0.png) -![UndoRedoState4](images/UndoRedoState4.png) +Step 2: `EditTaskCommandParser` will call `ParseUtil#parseIndex()` to get the task index. +Then `EditTaskCommandParser` will create `EditTaskDescriptor editTaskDescriptor`. `EditTaskCommandParser` will check if the +task name, datetime, link or tag prefix exist. It is optional to not have all the prefixes as user may not want to change certain field. +For each prefix in the command, it will set the value to `editTaskDescriptor`. +![EditTaskSequenceDiagramstate1](images/EditTaskDiagram/EditTaskSequenceDiagramState1.png) -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. +Step 3: `EditTaskCommandParser` will create `EditTaskCommand`, parse in `index` and `editTaskDescriptor` +`EditTaskCommand` will start to execute and call `ModelManager#setTask` and `ModelManager#updateFilteredTaskList` to update +the task and task list. +![EditTaskSequenceDiagramstate2](images/EditTaskDiagram/EditTaskSequenceDiagramState2.png) -![UndoRedoState5](images/UndoRedoState5.png) +Step 4: Lastly return the result. +Possible outcome from the result. +* Outcome 1: Successfully updated task. +* Outcome 2: Throw CommandException due to index out of range, invalid parameters or no valid changes to `Task`. -The following activity diagram summarizes what happens when a user executes a new command: +![EditTaskOutcomeActivityeDiagram](images/Activity Diagram/EditTaskOutcome.png) - +The Sequence Diagram below illustrates the overall interactions of how the edit task feature work. +![EditTaskSequenceDiagram](images/EditTaskSequenceDiagram.png) + +
+ +### View Task feature +The view task mechanism is facilitated by `ViewCommand`, `ViewCommandParser`, `ModelManager` and `Task`. Additionally, it implements the following operation: + +* `ViewCommandParser#parse()` — Parses the arguments provided by the users into a command to be executed. +* `ViewCommand#execute()`  — Executes the operations required to display the people associated with a specific task. +* `ModelManager#getFilteredTaskList()`  — Gets the task list currently displayed as output to the user. +* `ModelManager#updateFilteredPersonList()`  — Updates the person list displayed as output to the user by providing the argument with a list of people. +* `Task#getPeople()` — Gets a list of people associated to a task. + +Given below is an example usage scenario and how the view task mechanism behaves at each step. + +Step 1: The user will enter the command `view 1` to view the people associated with the first task. The command will be handled by +`AddressBookParser#parseCommand()` which will create a `ViewCommandParser` object. + +Step 2. The `AddressBookParser` will call `ViewCommandParser#parse()` which will parse the command, returning a `ViewCommand` to be executed. + +Step 3. The `LogicManager` will call `ViewCommand#execute()` which will execute the command. It will retrieve the task list + +by calling `ModelManager#getFilteredTaskList()` and retrieve the first `Task` from this list. + +Step 4: Afterwards, the `ViewCommand` will call `Task#getPeople()` to obtain the list of people associated with the `Task` and pass this list as an argument to +`ModelManager#updateFilteredPersonList()` which will proceed to update the UI. + +The following sequence diagram shows how the view task operation works: +![ViewSequenceDiagram](images/ViewSequenceDiagram.png) + +
:information_source: **Note:** The lifeline for `EditTaskCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
#### Design considerations: -**Aspect: How undo & redo executes:** +**Aspect:** How should the results be displayed in the *Contact* column when no contacts are associated with the task: -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +* **Alternative 1 (current choice):** Continue displaying the current list of people. + * Pros: Reduce commands required by user to populate and use the column for input. + * Cons: May be confusing to user. + +* **Alternative 2:** Display an empty list. + * Pros: Clearly inform the users that the task has no people associate with it. + * Cons: Requires more commands by the user in order to use the column again. + +### Find Task feature +The find task mechanism is facilitated by `FindTaskCommand`, `FilterCommandParser`, `ModelManager` and `Task`. Additionally, it implements the following operation: +* `FilterCommandParser#parse()` — Parses the arguments provided by the users into a command to be executed. +* `FindTaskCommand#execute()`  — Executes the operations required to display the task that matches the search keywords. +* `ModelManager#getFilteredTaskList()`  — Gets the task list currently displayed as output to the user. +* `ModelManager#updateFilteredTaskList()`  — Updates the task list displayed as output to the user by providing the argument with a list of task. -* **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. +Given below is an example usage scenario and how the view task mechanism behaves at each step. -_{more aspects and alternatives to be added}_ +Step 1. The user will enter the command `findt brush` to find all tasks that have the keyword `brush`. The command will be handled by +`AddressBookParser#parseCommand()` which will create a `FilterCommandParser` object. -### \[Proposed\] Data archiving +Step 2. The `AddressBookParser` will call `FilterCommandParser#parse()` which will parse the command, returning a `FindTaskCommand` to be executed. -_{Explain here how the data archiving feature will be implemented}_ +Step 3. The `LogicManager` will call `FindTaskCommand#execute()` which will execute the command. It will update the existing task list to only show +the task with the search keywords by calling `ModelManager#updateFilteredTaskList()`. +Step 4. Finally, the `FindTaskCommand` will return the command result of how many tasks were found, by calling `ModelManager#getFilteredTaskList().size()` + + +
-------------------------------------------------------------------------------------------------------------------- @@ -256,100 +409,222 @@ _{Explain here how the data archiving feature will be implemented}_ ### Product scope **Target user profile**: +* NUS computing professors that: + * need to manage a large number of contacts + * need to categorise these contacts (e.g. by role - Teaching Assistant, Student - or by class groups) + * need to keep track of relevant contact details (e.g. Github username, email) + * need to keep track of the people involved in each task + * prefer desktop apps over other types + * prefers typing to mouse interactions + * is reasonably comfortable using CLI apps -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps - -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: NUS computing professors can easily organise their module-related tasks and relevant contact details in one place, boosting their efficiency and productivity. ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | -*{More to be added}* +| Priority | As a …​ | I want to …​ | So that I can…​ | +|----------|---------------------|--------------------------------------------------------|---------------------------------------------------------------------------| +| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | +| `* * *` | user | add a new person | manage my contacts more efficiently | +| `* * *` | user | add a new task | manage my schedule more efficiently | +| `* * *` | user | delete a person | remove entries that I no longer need | +| `* * *` | user | delete a task | remove the task I no longer need | +| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | +| `* * *` | user | assign people to tasks | easily keep track of the people involved in a task | +| `* * *` | user | unassign people from tasks | easily maintain the list of people involved in a task | +| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | +| `* * *` | user | tag my contacts | organize the contacts to look neater | +| `* * *` | user | filter my tasks by name or date | locate tasks without having to go through the entire list | +| `* * *` | user | view contact details | lookup important contact information | +| `* * *` | user | tag tasks | organise my tasks | +| `* * *` | user | assign and remove the task to/from my contacts | allocate my tasks to the specific contact as needed | +| `* * *` | user | view the contacts assigned to a task | lookup the information of the people assigned to a task | +| `* *` | lecturer | store the meeting links of my tasks | easily access the meetings when I need to | +| `* *` | user | import contact data from a csv file | easily initialize my contacts without having to type hundreds of commands | +| `* *` | user | generate emails of all the contacts assigned to a task | easily transfer the emails to my preferred email client to contact them | +| `* *` | computing professor | get a contact's Github username | lookup their Github profiles | +| `* *` | professor | group the students based on module | know which student is under which module | +| `*` | professor | add graded component of the module | track students' performance of the module | + +
### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is `NUS Classes` and the **Actor** is the `user`, unless specified otherwise) -**Use case: Delete a person** +### Use case: UC01 - Delete a person **MSS** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User requests to list persons +2. NUS Classes shows a list of persons +3. User requests to delete a specific person in the list +4. NUS Classes deletes the person - Use case ends. +Use case ends. **Extensions** * 2a. The list is empty. - - Use case ends. + * Use case ends. * 3a. The given index is invalid. + * 3a1. NUS Classes shows an error message. + * Use case resumes from step 3. + - * 3a1. AddressBook shows an error message. +### Use case: UC02 - Import contacts - Use case resumes at step 2. +**MSS** +1. User requests to import contacts, providing the filepath of the source data file. +2. NUS Classes adds the contacts to the contact list. -*{More to be added}* +Use case ends. -### Non-Functional Requirements +**Extensions** -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. +* 1a. NUS Classes can't find the file + * 1a1. NUS Classes shows an error message. + * Use case ends. +* 1b. NUS Classes detects that the file is of invalid format or is a directory + * 1b1. NUS Classes shows an error message. + * Use case ends. +* 1c. NUS Classes detects that some entries have invalid fields + * 1c1. NUS Classes informs the user of the invalid fields and the reason they are invalid + * 1c2. User fixes these fields + * Use case continues from step 1. -*{More to be added}* -### Glossary +### Use case: UC03 - See all scheduled tasks -* **Mainstream OS**: Windows, Linux, Unix, OS-X -* **Private contact detail**: A contact detail that is not meant to be shared with others +**MSS** +1. User requests to see all tasks +2. NUS Classes shows a list of scheduled tasks --------------------------------------------------------------------------------------------------------------------- +Use case ends. -## **Appendix: Instructions for manual testing** -Given below are instructions to test the app manually. + +### Use case: UC04 - Schedule a task with a group -
:information_source: **Note:** These instructions only provide a starting point for testers to work on; -testers are expected to do more *exploratory* testing. +**MSS** -
+1. User requests to create a task +2. NUS Classes creates the task +3. User requests to assign a contact to the task +4. NUS Classes assigns the contact to the task -### Launch and shutdown +Use case ends. -1. Initial launch +**Extensions** - 1. Download the jar file and copy into an empty folder +* 1a. NUS Classes detects that compulsory arguments are omitted (e.g. name or date-time) + * 1a1. NUS Classes shows an error message. + * Use case ends. + +* 3a. NUS Classes detects that invalid task or contact index is provided + * 3a1. NUS Classes shows an error message. + * Use case continues from step 3. + +* 4a. User wishes to assign more contacts to the task + * Use case continues from step 3. + + +### Use case: UC05 - Update a task - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. +**MSS** + +1. User requests to see all tasks (UC03) +2. User requests to update a task, providing the details of the fields to be edited and the index of the task +3. NUS Classes updates the task -1. Saving window preferences +Use case ends. - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. +**Extensions** + +* 2a. NUS Classes detects that the index provided is out of bounds/invalid + * 2a1. NUS Classes shows an error message. + * Use case ends. +* 2b. NUS Classes detects that the fields provided are invalid + * 2b1. NUS Classes shows an error message. + * Use case ends. +* 2c. NUS Classes detects that no optional arguments are provided + * 2c1. NUS Classes shows an error message. + * Use case ends. + +### Use case: UC06 - Generate emails of all contacts assigned to a task + +**MSS** + +1. User requests to see all tasks (UC03) +2. User requests to generate the emails of all contacts a task, and provides the index of the task +3. NUS Classes displays the emails and provides a button to copy the emails. + +Use case ends. + +**Extensions** + +* 1a. NUS Classes doesn't have any tasks created + * 1a1. NUS Classes shows an error message. + * Use case ends. +* 2a. NUS Classes detects that the index provided is invalid + * 2a1. NUS Classes shows an error message. + * Use case ends. +* 3a. User clicks the button to copy the emails. + * 3a1. User pastes the emails into their preferred email application + * Use case ends. + +### Use case: UC07 - Searching for tasks by name and/or tags + +**MSS** - 1. Re-launch the app by double-clicking the jar file.
- Expected: The most recent window size and location is retained. +1. User specifies the keyword to search for. +2. NUS Classes displays the tasks whose names and/or tags match the keyword. -1. _{ more test cases …​ }_ +Use case ends. + +### Use case: UC08 - Searching for tasks by date range + +**MSS** + +1. User specifies the date range to search for. +2. NUS Classes displays the tasks whose date(s) fit within the range. + +Use case ends. + +
+ + +### Non-Functional Requirements + +1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. +2. Should be able to hold up to 1000 persons and/or tasks without a noticeable sluggishness in performance for typical usage. +3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +4. App should not exit or shut down without user explicit command. +5. App should display relevant information within 2 seconds after user enter command. +6. The information stored should not change without user explicit command. +7. Should be able to download and use without installer. +8. Should be able to work and store information without any third party database system. + +## **Appendix: Instructions for manual testing** + +Given below are instructions to test the app manually. + +### Launch and shutdown + +1. Initial launch + + 1. Download the jar file and copy into an empty folder + + 2. Double-click the jar file Expected: Shows the GUI with a set of sample contacts and tasks. + +2. Shutdown + 1. Click the 'close' button or execute the `exit` command Expected: The app shuts down + ### Deleting a person @@ -358,20 +633,65 @@ testers are expected to do more *exploratory* testing. 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. + Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. 1. Test case: `delete 0`
Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
Expected: Similar to previous. - -1. _{ more test cases …​ }_ + ### Saving data -1. Dealing with missing/corrupted data files - - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ - -1. _{ more test cases …​ }_ +1. Data (contacts and tasks) should be automatically saved when the app is shut down. + 1. Test case: `addc n/Test Person p/123456 e/e1234@u.nus.edu u/test123`, then `exit` and restart the app.
Expected: The created contact is saved and is shown. + 2. Test case: `addt tn/Test task dt/12-12-2020 1234`, then `exit` and restart the app.
Expected: The created task is saved and is shown. + +### Importing contacts from a .csv file +1. The import feature should work with a valid .csv file. + 1. Test case: + 1. Create a .csv file with the filename `test.csv` using a text editor and place it into the NUS Classes folder. + 2. The .csv file should contain the headers `Name,Phone,Github,Email,Tags` + 3. On the next line, add the sample contact `Alex Bean,91234567,alexbean@gmail.com,Lab 12F/Student` + 4. Execute the command `import fp/test.csv` + 5. The NUS Classes should inform you that the contact has been successfully imported, and the contact is displayed in the contact list. + 2. For advanced testing, you may rename the file and place it in a subdirectory. + * make sure to specify the filepath relative to the jar directory, specified according your OS's file system. + +
+ +## Prefix summary + +| Prefix | Meaning | Constraints | Used in these commands | +|-----------|-------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| `n/` | Name of contact | Can only contain alphabetical letters and selected symbols like `/`, `-`, `SPACE` and `'`. | `addc`, `editc` | +| `p/` | Phone number of contact OR Index of person assigned | For `PHONENUMBER`, it is only used in `addc` and `editc`. Only numbers are allowed and must contain at least 3 digits.

For `PERSONINDEX`, it is only used in `assign` and `unassign`. Only valid, positive non-zero integers are allowed. | `addc`, `editc` (as `PHONENUMBER`)

`assign`, `unassign` (as `PERSONINDEX`) | +| `e/` | Email of contact | An email address should begin with a local part containing alphanumeric characters and these special characters: +_.-. The local part cannot start with a special character. This should be followed by a '@' and then a domain name.

The domain name should be made up of domain labels separated by periods, and must end with a domain label at least 2 characters long and each domain label can only consist of alphanumeric characters, separated only by hyphens, if any. | `addc`, `editc` | +| `u/` | Github Username of contact | Github usernames can only consist of alphanumeric characters or hyphens as per Github conventions. | `addc`, `editc` | +| `t/` | Tag of either contact or task | A Task cannot contain two duplicate tags. | `addc`, `editc`, `addt`, `editt` | +| `INDEX` | Index of the task or contact specified | `INDEX` refers to the index number as listed in NUS Classes, e.g. `INDEX` of `1` would mean the first task/contact. | `editc`, `deletec`, `editt`, `assign`, `unassign`, `view`, `mark`, `unmark`, `deletet`, `gen` | +| `KEYWORD` | Keyword of the task or contact. This can be the name or tag or tasks/contacts | `KEYWORD` is case-insensitive. Orders of `KEYWORD`s do not matter. Only full words will be matched. | `findc`, `findt` | +| `tn/` | Task name of the task | Two tasks with the same `TASKNAME` is valid. | `addc`, `editc` | +| `dt/` | Date and time of the task. Can include both start and end times. | Needs to be in the format dd-mm-yyyy hhmm | `addt`, `editt`, `findt` | +| `z/` | Link to online meeting/video conferencing. | Only valid links are accepted, with `https://` or `http://`. | `addt`, `editt` | +| `r/` | Interval and recurrence of the task. | Pre-set values like `daily`, `weekly`, `monthly`, `quarterly` and `annually` are accepted. Else, only positive non-zero integers are valid. | `addt` | +| `all/` | Shows all tasks in the list. | Only used in `listt`. | `listt` | +| `c/` | Shows all completed tasks in the list. | Only used in `listt`. | `listt` | +| `nc/` | Shows all uncompleted tasks in the list. | Only used in `listt`. | `listt` | + + +## **Glossary** + +| Term | Meaning | +|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| API | Application Programming Interface. Enables different systems to interact with each other programmatically | +| .csv | A plain text file containing a list of data, separated by commas | +| MSS | Main Success Scenario, the action steps of a typical scenario where the goal is delivered | +| NUS | National University of Singapore | +| NUS Classes | The name of the application | +| Mainstream OS | Windows, Linux, MacOS | +| UI | User Interface, the means by which the user interacts with the system | +| CLI | Command-line interface, which processes text-based commands from the user | +| GUI | Graphical user interface, a visual way of interacting with a computer using items such as windows, icons and menus | +| Jar | A JAR (Java ARchive) is a package file format typically used to aggregate many Java class files and associated metadata and resources (text, images, etc.) into one file for distribution. | diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 275445bd551..a08128fa17c 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -1,10 +1,6 @@ ---- -layout: page -title: Setting up and getting started ---- - -* Table of Contents -{:toc} +## Table of Contents +* [Setting up the project in your computer](#setting-up-the-project-in-your-computer) +* [Before writing code](#before-writing-code) -------------------------------------------------------------------------------------------------------------------- @@ -12,7 +8,6 @@ title: Setting up and getting started ## Setting up the project in your computer
:exclamation: **Caution:** - Follow the steps in the following guide precisely. Things will not work out if you deviate in some steps.
@@ -23,7 +18,7 @@ If you plan to use Intellij IDEA (highly recommended): 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. **Verify the setup**: - 1. Run the `seedu.address.Main` and try a few commands. + 1. Run `seedu.address.Main` and try a few commands. 1. [Run the tests](Testing.md) to ensure they all pass. -------------------------------------------------------------------------------------------------------------------- @@ -45,7 +40,7 @@ If you plan to use Intellij IDEA (highly recommended): 1. **Learn the design** - When you are ready to start coding, we recommend that you get some sense of the overall design by reading about [AddressBook’s architecture](DeveloperGuide.md#architecture). + When you are ready to start coding, we recommend that you get some sense of the overall design by reading about [NUS Classes’ architecture](DeveloperGuide.md#architecture). 1. **Do the tutorials** These tutorials will help you get acquainted with the codebase. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 3716f3ca8a4..ca14dd17724 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -2,11 +2,58 @@ layout: page title: User Guide --- - -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. - -* Table of Contents -{:toc} +NUS Classes is a desktop app for NUS Computing professors to manage their tasks and contacts. It includes task management features such as +creating tasks, tagging tasks, assigning contacts to tasks, and marking tasks as complete or incomplete. It also includes contact management features such as finding contacts, assigning contacts to specific tasks and tagging contacts. + +NUS Classes also provides a simple alert feature for tasks by displaying tasks in different color based on the urgency of the task. Tasks that are overdue are marked as red, whereas, tasks that are nearing deadline are marked as yellow. + +NUS Classes is optimized for use via a Command Line Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). +Using NUS Classes can get your contact management tasks done faster than traditional GUI apps, saving time on otherwise tedious administrative tasks. + +This User Guide has been conveniently catergorised into **Contact Features**, **Task Features** and **Other Features**. This will help you locate quickly the command that you want to know more about. + +Blue text boxes are additional tips and notes to enhance your experience with NUSClasses, while yellow text boxes are warnings that will cause NUSClasses to not function as intended. Graphics are also available to further aid us visually. + +Hope you'll be satisfied using NUSClasses :smile: + +
+ +## Table of Contents + +* [Quick Start](#quick-start) +* [Features](#features) + * [Help](#viewing-help--help) + * [Contact Features](#contact-features) + * [Adding a contact](#adding-a-contact-addc) + * [Listing all contacts](#listing-all-contacts--listc) + * [Editing a contact](#editing-a-contact--editc) + * [Finding contacts](#finding-contacts-findc) + * [Deleting a contact](#deleting-a-contact--deletec) + * [Task Features](#task-features) + * [Adding a task](#adding-a-task-addt) + * [Listing tasks](#listing-tasks--listt) + * [Editing a task](#editing-a-task-editt) + * [Finding tasks](#finding-tasks-findt) + * [Finding tasks by name or tag](#1-finding-tasks-by-name-or-tag-findt) + * [Finding tasks by date](#2-finding-tasks-by-date-findt) + * [Assigning a contact to a task](#assigning-a-contact-to-a-task-assign) + * [Viewing contacts assigned to a task](#viewing-contacts-assigned-to-a-task-view) + * [Unassigning a contact from a task](#unassigning-a-contact-from-a-task-unassign) + * [Mark a task as done](#marking-a-task-as-done-mark) + * [Unmark a task as not done](#unmarking-a-task-as-not-done-unmark) + * [Deleting tasks](#deleting-a-task-deletet) + * [Generating emails of all contacts assigned to a task](#generating-the-emails-of-all-the-contacts-assigned-to-a-task-gen) + * [Other Features](#other-features) + * [Importing contacts from a data file](#importing-contacts-from-a-data-file--import) + * [Clearing all data](#clearing-all-data--clear) + * [Exiting the program](#exiting-the-program--exit) + * [Saving the data](#saving-the-data) + * [Editing the data file](#editing-the-data-file) +* [FAQ](#faq) +* [Prefix Summary](#prefix-summary) +* [Command Summary](#command-summary) + +
-------------------------------------------------------------------------------------------------------------------- @@ -14,41 +61,46 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo 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). +1. Download the latest `NUSClasses.jar` from [here](https://github.com/AY2122S2-CS2103T-T12-4/tp/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +1. Copy the file to the folder you want to use as the _main folder_ for your NUS Classes manager. -1. Double-click the file to start the app. The GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
+1. Double-click the file to start the app. The GUI similar to below should appear in a few seconds. Note how the app contains some sample data.
![Ui](images/Ui.png) -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
+1. Type the command in the command box and press `Enter` to execute it. e.g. typing **`help`** and pressing `Enter` will open the help window.
Some example commands you can try: - * **`list`** : Lists all contacts. + * **`listc`** : Lists all contacts. + + * **`addc`**`n/John Doe p/98765432 e/johnd@u.nus.edu u/john123 t/Schoolmate` : Adds a contact named `John Doe` to NUS Classes, + with email `johnd@u.nus.edu`, Github Username `john123` and tag `Schoolmate` - * **`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. + * **`deletec`**`3` : Deletes the 3rd contact shown in the current contact list. - * **`delete`**`3` : Deletes the 3rd contact shown in the current list. + * **`assign`**`1 p/ 2` : Assigns the contact at index 2 to the task at index 1. - * **`clear`** : Deletes all contacts. + * **`clear`** : Clears all contacts and tasks from NUS Classes * **`exit`** : Exits the app. 1. Refer to the [Features](#features) below for details of each command. +
+ -------------------------------------------------------------------------------------------------------------------- -## Features +# Features
**:information_source: Notes about the command format:**
-* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. +* Words in `UPPER_CASE` are the parameters to be supplied by you.
+ e.g. in `addc n/CONTACTNAME`, `CONTACTNAME` 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. `addc n/CONTACTNAME p/PHONENUMBER e/EMAIL u/GIT_USERNAME [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. @@ -59,134 +111,572 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo * If a parameter is expected only once in the command but you specified it multiple times, only the last occurrence of the parameter will be taken.
e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` will be taken. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
+* Extraneous parameters for commands that do not take in parameters (such as `help`, `listc`, `exit` and `clear`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. +* `INDEX` refers to a single non-zero positive integer only, and within the bounds of the number of contacts/number of tasks. Values such as `-1`, `abc`, or `2 abcdefg` are invalid. + +* Some examples have images of the usage of the commands in the User Interface. This is to allow you to easily visualize and understand what the commands do in that context. +
+
+ ### Viewing help : `help` -Shows a message explaning how to access the help page. +Shows a message with the link to the User Guide for reference. + +![help message](images/helpmessage2.png) + +**Format**: `help` + +## Contact Features + +### Adding a contact: `addc` + +Adds a contact with basic details like name, phone number, email and Github Username. Tags are optional but can be added as well for easier contact management. -![help message](images/helpMessage.png) +**Format**: `addc n/CONTACTNAME p/PHONENUMBER e/EMAIL u/GITHUB_USERNAME [t/TAG]…​` -Format: `help` +**Examples**: +* `addc n/john p/12345678 e/john@nus.edu.sg u/john123 t/Schoolmate` +* `addc n/mary p/87654321 e/mary@gmail.com u/maryCS t/Teammate t/Classmate` -### Adding a person: `add` -Adds a person to the address book. +
:bulb: **Notes:** -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +* Contacts with the same names are allowed. Some modules may be big, so there may be students with the same name! + +* You can add multiple tags to a contact for easier management, as some students/TAs might be taking your other modules too. Just put t/ before every tag! + +* Make sure the contact's email is in the correct format, i.e. abd@gmail.com :white_check_mark:, abd@yahoo :x: + +* Make sure the contact's phone number is at least 3 digits long! Phone numbers with > 8 digits are allowed since international phone numbers have different numbers of digits. + +* Github usernames can only consist of alphanumeric characters or single hyphens, and cannot begin or end with a hyphen. Usernames such as `john123` and `jo-hn123` are valid, but usernames such as `john123-`, `-john123` and `jo--hn123` are invalid. -
:bulb: **Tip:** -A person can have any number of tags (including 0)
-Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +### Listing all contacts : `listc` -### Listing all persons : `list` +Shows a list of all contacts in NUS Classes. -Shows a list of all persons in the address book. +**Format**: `listc` -Format: `list` +
:bulb: **Tip:** +You will find this command useful after using [`findc`](#finding-contacts-by-name-findc) +
-### Editing a person : `edit` +### Editing a contact : `editc` -Edits an existing person in the address book. +Edits an existing contact in NUS Classes. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +**Format**: `editc INDEX [n/NAME] [p/PHONE] [e/EMAIL] [u/GITHUB_USERNAME] [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, …​ +* Edits the contact at the specified `INDEX`. +* The index refers to the index number shown in the displayed contact list. * At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. -Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +
:exclamation: **Caution:** +* The index **must be a positive integer** 1, 2, 3, …​ + +* When editing tags, the existing tags of the contact will be removed i.e. adding of tags is not cumulative. +
+ +**Examples**: +* `editc 2 n/Betsy Crower t/` Edits the name of the 2nd contact to be `Betsy Crower` and clears all existing tags. +* `editc 1 p/82223333 e/Joseph@comp.nus.edu.sg` Edits the phone number and email address of the 1st contact to be `82223333` and `Joseph@comp.nus.edu.sg` respectively. +

+ ![Result for 'editc'](images/editcCommandShowcase.png) + +
:bulb: **Tip:** +You can remove all the contact’s tags by typing `t/` without specifying any tags after it. Useful when removing outdated tags from a contact. +
-### Locating persons by name: `find` +### Finding contacts: `findc` -Finds persons whose names contain any of the given keywords. +Find contacts whose names and/or tags contain any of the given keywords. -Format: `find KEYWORD [MORE_KEYWORDS]` +**Format**: `findc KEYWORD [MORE_KEYWORDS]…​` -* The search is case-insensitive. e.g `hans` will match `Hans` +* `KEYWORD` can be used to search for matching words in both tags and names of contacts. +* The search is case-insensitive. e.g `john` will match `John` * The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. * Only full words will be matched e.g. `Han` will not match `Hans` * Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` + e.g. `hans mary` will return `Hans Bo`, `Mary Sue` + +**Examples**: +* `findc John` returns `john` and `John Doe`. +* `findc brian lecturer` returns `Brian Chow`, `Joseph` who has a tag of `Lecturer`.
+
+ ![result for 'find brian sean'](images/findcCommandShowcase.png) + + +### Deleting a contact : `deletec` + +Deletes the specified contact from NUS Classes. + +**Format**: `deletec INDEX` + +* Deletes the contact at the specified `INDEX`. +* The index refers to the index number shown in the displayed contact list. + +
:exclamation: **Caution:** +The index **must be a positive integer** 1, 2, 3, …​ +
+ +**Examples**: +* `listc` followed by `deletec 2` deletes the 2nd contact in NUS Classes. +* `findc Joseph` followed by `deletec 1` deletes the 1st contact in the results of the `findc` command.
+
+ ![result for `deletec 1` part 1](images/deletecCommandShowcase1.png)

+ ![result for `deletec 1` part 2](images/deletecCommandShowcase2.png) + +
+ +## Task Features + +### Adding a task: `addt` + +Adds a task for a datetime with a tag. + +**Format**: `addt tn/TASKNAME dt/DATETIME [ENDDATETIME] [t/TAG]…​ [z/LINK] [r/INTERVAL RECURRENCE]` + +* The format for TIME is in `dd-MM-yyyy HHmm`. + +
:exclamation: **Caution:** +The task that you are creating cannot have duplicate tags, tags are unique for that specific task e.g. `addt tn/Lecture dt/12-12-2022 1200 t/Weekly Lecture t/Weekly Lecture` would be invalid. + +
+ +
-Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +:bulb: **Notes about `TASK`:**
+* Two Tasks with the same `TASKNAME` is valid. This is to allow for multiple tasks with different dates to be valid, such as `CS2103T Lecture` +at `04/04/2022` and another `CS2103T Lecture` at `11/04/2022`. + +* You can create overdue Tasks (with `DATETIME` before today) if you wish to remind yourself of overdue tasks! :smiley: +
+ +
+ +:bulb: **Notes about `INTERVAL RECURRENCE`:**
+ +* `INTERVAL` refers to the number of days for next occurrence of the task:
+_e.g. `INTERVAL` = 5 (days) for a task on 01-01-2022 would next occur on 05-01-2022._ +* For `INTERVAL`, the values `daily`,`weekly`,`monthly`, `quarterly` and `annually` are accepted. +* `RECURRENCE` refers to how many cycles the task is to be repeated:
+_e.g. `RECURRENCE` = 5 (cycles) with an `INTERVAL` = 7 (equivalent to `weekly`) means that the task repeats weekly for 5 cycles._ + +
+ +**Examples**: +* `addt tn/Meeting dt/17-03-2022 1800 t/School` Adds a task called Meeting for `17th March 2022, 6pm` at School. +* `addt tn/Consultation dt/19-03-2022 1500, 19-03-2022 1600` Adds a task called Consultation taking place from `19th March 2022 3-4pm`. +* `addt tn/CS2103 Lecture dt/19-03-2022 1500, 19-03-2022 1600 z/https://nus-sg.zoom.us…​ r/weekly 12` +Adds a task called CS2103 Lecture taking place from `19th March 2022 3-4pm` that occurs `weekly` for `12 cycles` with the `meeting link`. +* `addt tn/Running dt/12-02-2022 1900 t/Track r/3 5` Adds a task called Running for `12th February 2022, 7pm` that occurs `every 3 days` for `5 cycles`. + +### Editing a task: `editt` +Edits an existing task in the task list. + +**Format**: `editt INDEX [tn/TASKNAME] [dt/DATETIME, ENDDATETIME*] [z/LINK] [t/TAG]…​` + +* Edits the task at the specified `INDEX`. The index refers to the index number shown in the displayed task list. +* At least one of the optional fields must be provided. +* Existing values will be updated to the input values. + +
:exclamation: **Caution:** +The index **must be a positive integer** 1, 2, 3, …​ +
+ +**Examples**: +* `editt 1 tn/Meeting with TAs` Edits the name of the 1st displayed task to be `Meeting with TAs`. +* `editt 2 tn/Meeting with Prof Tan dt/01-12-2022 1200, 01-12-2022 1300` Edits the name of the 2nd displayed task to be `Meeting with Profs Tan` and the date to be `01 Dec 2022, 12pm-1pm`.
+
+ ![`editt 2 tn/Meeting with Prof Tan dt/01-12-2022 1200, 01-12-2022 1300`](images/edittCommandShowcase1.png) + + +* `editt 1 dt/12-12-2022 1200, 12-12-2022 1400` Edits the datetime of the 1st displayed task to be on `12 Dec 2022, 12-2pm`.
+
+ ![`editt 1 dt/12-12-2022 1200, 12-12-2022 1400`](images/edittCommandShowcase2.png) + + +
+:bulb: **Tips:**
+ +* If there's no need to change a certain field you can leave it out!
+* The `ENDDATETIME` field is optional. +
+ +### Finding tasks: `findt` + +You can find tasks either by 1) name and/or tags **OR** 2) date time range + +
:exclamation: **Caution:** + +If your `findt` command contains both name `KEYWORD` and date time prefix `dt/`, + the command will ignore the `KEYWORD` and `findt dt/` will take higher precedence. +
+e.g. `findt meeting dt/12-12-2022 1200, 13-12-2022 1200` will be equivalent to +`findt dt/12-12-2022 1200, 13-12-2022 1200` +
+ +#### 1. Finding tasks by name or tag: `findt` + +Find tasks whose task names or tags contain any of the given keywords. + +**Format**: `findt KEYWORD [MORE_KEYWORDS]…​` +* `KEYWORD` can be used to search for matching words in both tags and names of tasks. +* The search is case-insensitive. e.g `lecture` will match `Lecture` +* The order of the keywords does not matter. e.g. `Lecture CS2103T` will match `CS2103T Lecture` +* Only full words will be matched e.g. `Tut` will not match `Tutorial` +* Tasks matching at least one keyword will be returned (i.e. `OR` search). + e.g. `Lecture Tutorial` will return `CS2103T Lecture`, `Tutorial 12` + +**Examples**: +* `findt with` returns `Consultation with students` and `Meeting with invigilators`. +* `findt week` returns all recurring instances of `CS2103T Lecture` as their tags contain `Week XX Lecture`. +* `findt TAs lecture` returns `Meeting with TAs` and all recurrences of `CS2103T lecture`.
+
+ ![`findt TAs lecture`](images/findtCommandShowcase.png) + +
+:bulb: **Tip:** + +If you have many tasks, using `findt` will help you to keep organized! + +
+ +#### 2. Finding tasks by date: `findt` +Find tasks whose task falls in between the given dates (inclusive). + +**Format**: `findt dt/DATETIME1, DATETIME2` + +* The ordering of date doesn't matter . e.g `dt/12-02-2022 0800, 13-03-2022 0800` will match `dt/13-02-2022 0800, 12-03-2022 0800` +* `DATETIME1` and `DATETIME2` follows this format: _dd-MM-yyyy HHmm_ +* _dd_: Day; _MM_: Month; _yyyy_: Year; _HH_: Hour; _mm_: Minutes +* `HHmm` is in 24-Hour format +* Requires 2 date time inputs + +**Examples**: +* `findt dt/14-04-2022 0900, 15-04-2022 0900` Finds all tasks in between `14th April 2022, 9am` and `15th April 2022, 9am`, inclusive. +* `findt dt/15-02-2022, 13-02-2022` Finds all tasks in between `13th February 2022, 12mn` and `15th February 2022, 11:59pm`, inclusive. +* `findt dt/20-12-2022, 21-12-2022 0900` Finds all tasks in between `20th December 2022 12mn` and `21st December 2022 9am`, inclusive. + +
+:bulb: **Tip:** + +`DATETIME1` and `DATETIME2` do not require `HHmm` if you wish to find tasks that fall on/in between the dates. + +
-### Deleting a person : `delete` +### Assigning a contact to a task: `assign` +Assigns a contact in the contact list to a task. -Deletes the specified person from the address book. +**Format**: `assign INDEX p/CONTACTINDEX` -Format: `delete INDEX` +* Assigns the contact at the specified `CONTACTINDEX` to the task at `INDEX`. +* The indices refer to the index numbers shown in the corresponding displayed task/contact list. -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. +
:exclamation: **Caution:** +The index **must be a positive integer** 1, 2, 3, …​ +
+ +**Examples**: +* `assign 1 p/2` Assigns the 2nd contact in the contact list to the 1st task in the task list.
+
+ ![`assign 1 p/2](images/assignCommandShowcase.png) + +### Viewing contacts assigned to a task: `view` + +Display all contacts assigned to a given task. + +**Format**: `view TASKINDEX` + +* View all the contact assigned to the task located the specified `INDEX` +* The index refers to the index number shown in the displayed task list. + +
:exclamation: **Caution:** +The index **must be a positive integer** 1, 2, 3, …​ +
+ +**Examples**: +* `view 2` will display all contacts assigned to the 1st task in the task list.
+
+ ![`view 2`](images/viewtCommandShowcase.png) + + +### Unassigning a contact from a task: `unassign` +Unassigns a contact in the contact list from a task. + +**Format**: `unassign INDEX p/CONTACTINDEX` + +* Unassigns the contact at the specified `CONTACTINDEX` to the task at `INDEX`. +* The indices refer to the index numbers shown in the corresponding displayed task/contact list. +* The `view` command can help you quickly identify which contacts are already assigned to a task. + +
+ +:exclamation: **Caution:**
* The index **must be a positive integer** 1, 2, 3, …​ +* If the contact is not already assigned to the task, the operation will fail. +
+ +**Examples**: +* `unassign 1 p/2` Unassigns the 2nd contact in the contact list from the 1st task in the task list.
+
+ ![unassign1](images/unassignCommandShowcase.png) + + +### Marking a task as done: `mark` + +Marks the specified task from the task list as done. + +**Format**: `mark INDEX` + +* Marks the task at the specified `INDEX`. +* The index refers to the index number shown in the displayed task list. +* Icon will display a green tick to show the task is marked. + +
:exclamation: **Caution:** +The index **must be a positive integer** 1, 2, 3, …​ +
+ +**Examples**: +* `mark 2` marks the task at index 2 as done.
+
+ ![`mark 2`](images/marktCommandShowcase.png) + +### Unmarking a task as not done: `unmark` + +Unmarks the specified task from the task list as not done. + +**Format**: `unmark INDEX` + +* Unmarks the task at the specified `INDEX`. +* The index refers to the index number shown in the displayed task list. +* Icon will display an empty white box to show the task is unmarked. + +
:exclamation: **Caution:** +The index **must be a positive integer** 1, 2, 3, …​ +
+ +**Examples**: +* `unmark 2` unmarks the task at index 2 as not done.
+
+ ![`unmark 2`](images/unmarkCommandShowcase.png) + +### Listing tasks : `listt` + +Shows a list of all the tasks in the task list as per the specified filtering options. `listt` has the three following formats: + +**Format**: + +`listt all/` +Shows a list of all tasks in the task list. + +`listt c/` +Shows a list of tasks that is marked as completed in the task list. + +`listt nc/` +Shows a list of tasks that is not mark as completed in the task list. + + +
+**:information_source: Information:** +
    +
  • If more than 1 prefixes is present, system will prioritise `all/` -> `nc/` -> `c/` .
  • +
+
+ +### Deleting a task: `deletet` -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. +Deletes the specified task from the task list. -### Clearing all entries : `clear` +**Format**: `deletet INDEX` -Clears all entries from the address book. +* Deletes the task at the specified `INDEX`. +* The index refers to the index number shown in the displayed task list. + +
:exclamation: **Caution:** +The index **must be a positive integer** 1, 2, 3, …​ +
+ + +**Examples**: +* `listt all/` followed by `deletet 1` lists out all the tasks in NUS Classes, then deletes the task at index 1. +* `findt lecture` followed by `deletet 2` lists out all tasks with the keyword `lecture`, then deletes the task at index 2.
+
Finding the tasks by keyword `lecture`: + ![`findt lecture` followed by `deletet 2`](images/deletettCommandShowcase1.png) +
+
Deleting the lecture at index 2 `deletet 2`: + ![`deletet 2`](images/deletetCommandShowcase2.png) + + +### Generating the emails of all the contacts assigned to a task: `gen` + +Displays all the emails of all the contacts assigned to the specified task and displays a button to copy the emails into your clipboard. + +**Format**: `gen INDEX` + +* Displays all the emails of the contacts assigned to the task at the specified `INDEX`. +* The index refers to the index number shown in the displayed task list. + +
:exclamation: **Caution:** +The index **must be a positive integer** 1, 2, 3, …​ +
+ + +**Examples**: +* `gen 1` displays all the emails of the contacts assigned to the task at index 1.
+
+ ![`gen 1`](images/genCommandShowcase.png) + + +
+ +# Other Features + +### Clearing all data : `clear` + +Clears all contacts and tasks from NUS Classes. + +**Format**: `clear` + +
:exclamation: **Caution:** +Be careful! This action is irreversible. +
-Format: `clear` ### Exiting the program : `exit` Exits the program. -Format: `exit` +**Format**: `exit` ### Saving the data -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +NUS Classes data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. ### Editing the data file -AddressBook data are saved as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +NUS Classes data are saved into two JSON files: +* `[JAR file location]/data/addressbook.json` for contact data +* `[JAR file location]/data/tasklist.json` for task data + +Advanced users are welcome to update data directly by editing these 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. +If your changes to the data file makes its format invalid, NUS Classes will discard all data and start with an empty data file at the next run.
-### Archiving data files `[coming in v2.0]` +### Importing contacts from a data file : `import` + +Imports a list of contacts from a .csv file. + +**Format**: `import fp/FILEPATH` + +* The .csv file must contain the following 5 headers (Name, Phone, Email, Github, Tags). Any other headers will be ignored. +* If there are repeated headers, the first one will be considered. +* Tags in the .csv file should be separated with a `/` character. +* Invalid csv entries (e.g. due to invalid or duplicate fields) will be skipped, but valid entries will still be added. + +**Examples**: +* `import fp/data/data.csv` (macOS/Linux) / `import fp/data\data.csv` (Windows) will import all valid entries from the `data.csv` folder in the `/data` directory of the NUS Classes folder. +* `import fp/contacts.csv` (all supported platforms) will import all valid entries from the `contacts.csv file` in the NUS Classes root folder. +

+ ![`importCommand1` followed by `deletet 2`](images/importCommandShowcase1.png) +

+ ![`importCommand2`](images/importCommandShowcase2.png) + + + +### User-friendly date display + +Instead of always displaying dates in full (e.g. DD MM YYYY), our dates will be displayed based on the current day to be more user-friendly. Here's a reference if you get confused: + +| Displayed date | Explanation | +|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Today` | The current calendar day. | +| `Tomorrow` | The next calendar day. | +| Day of week (e.g. `Mon`, `Sat`) | The next occurrence of that day of week. Examples:
- If today is `7 Apr, Thu`, `Tue` will refer to the _next_ Tuesday `12 Apr, Tue`
- If today is `4 Apr, Mon`, `Mon` will refer to the _next_ Monday `11 Apr, Mon` | +| Day and month (without year) | The day of the current calendar year (e.g. if the current calendar year is 2022, `11 Apr` refers to `11 Apr 2022` | +| Day, month and year | - | + -_Details coming soon ..._ +
-------------------------------------------------------------------------------------------------------------------- ## FAQ **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**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 NUS Classes home folder. + +**Q**: Do I need to have extensive knowledge about Command Line Interface (CLI) to use this?
+**A**: Nope! Simply refer to the format given in our [Command Summary](#command-summary) and our [Features Section](#features) for guidance! + +**Q**: I have further questions about NUS Classes. Where do I ask?
+**A**: Open an Issue in our team's [Issue Tracker](https://github.com/AY2122S2-CS2103T-T12-4/tp/issues) and we'll look into it! :smiley: + +**Q**: Who do I contact if I want to be part of the Developer team?
+**A**: You can email us at _e0544441@u.nus.edu_ with the subject `Interest in joining NUSClasses Developer Team` + +**Q**: What do I do to update the app?
+**A**: Uninstall the current version of the app. Then, download the latest version of `NUSClasses.jar` from our [GitHub releases](https://github.com/AY2122S2-CS2103T-T12-4/tp/releases) + +
-------------------------------------------------------------------------------------------------------------------- +## Prefix summary + +| Prefix | Meaning | Constraints | Used in these commands: | +|-----------|-------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| `n/` | Name of contact | Can only contain alphabetical letters and selected symbols like `/`, `-`, `SPACE` and `'`. | `addc`, `editc` | +| `p/` | Phone number of contact OR Index of person assigned | For `PHONENUMBER`, it is only used in `addc` and `editc`. Only numbers are allowed and must contain at least 3 digits.

For `PERSONINDEX`, it is only used in `assign` and `unassign`. Only valid, positive non-zero integers are allowed. | `addc`, `editc` (as `PHONENUMBER`)

`assign`, `unassign` (as `PERSONINDEX`) | +| `e/` | Email of contact | An email address should begin with a local part containing alphanumeric characters and these special characters: +_.-. The local part cannot start with a special character. This should be followed by a '@' and then a domain name.

The domain name should be made up of domain labels separated by periods, and must end with a domain label at least 2 characters long and each domain label can only consist of alphanumeric characters, separated only by hyphens, if any. | `addc`, `editc` | +| `u/` | Github Username of contact | Github usernames can only consist of alphanumeric characters or hyphens as per Github conventions. | `addc`, `editc` | +| `t/` | Tag of either contact or task | A Task cannot contain two duplicate tags. | `addc`, `editc`, `addt`, `editt` | +| `INDEX` | Index of the task or contact specified | `INDEX` refers to the index number as listed in NUS Classes, e.g. `INDEX` of `1` would mean the first task/contact. | `editc`, `deletec`, `editt`, `assign`, `unassign`, `view`, `mark`, `unmark`, `deletet`, `gen` | +| `KEYWORD` | Keyword of the task or contact. This can be the name or tag or tasks/contacts | `KEYWORD` is case-insensitive. Orders of `KEYWORD`s do not matter. Only full words will be matched. | `findc`, `findt` | +| `tn/` | Task name of the task | Two tasks with the same `TASKNAME` is valid. | `addc`, `editc` | +| `dt/` | Date and time of the task. Can include both start and end times. | Needs to be in the format dd-mm-yyyy hhmm | `addt`, `editt`, `findt` | +| `z/` | Link to online meeting/video conferencing. | Only valid links are accepted, with `https://` or `http://`. | `addt`, `editt` | +| `r/` | Interval and recurrence of the task. | Pre-set values like `daily`, `weekly`, `monthly`, `quarterly` and `annually` are accepted. Else, only positive non-zero integers are valid. | `addt` | +| `all/` | Shows all tasks in the list. | Only used in `listt`. | `listt` | +| `c/` | Shows all completed tasks in the list. | Only used in `listt`. | `listt` | +| `nc/` | Shows all uncompleted tasks in the list. | Only used in `listt`. | `listt` | + + +
+ ## Command summary -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +| Action | Format | +|------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| +| [**Add Contact**](#adding-a-contact-addc) | `addc n/NAME p/PHONE_NUMBER e/EMAIL u/GIT_USERNAME [t/TAG]…​` | +| [**List Contacts**](#listing-all-contacts--listc) | `listc` | +| [**Edit Contact**](#editing-a-contact--editc) | `editc INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [u/GITHUBUSERID] [t/TAG]…​` | + | [**Find Contacts**](#finding-contacts-findc) | `findc KEYWORD [MORE_KEYWORDS]…​` | +| [**Delete**](#deleting-a-contact--deletec) | `deletec INDEX` | +| [**Add Task**](#adding-a-task-addt) | `addt tn/TASKNAME dt/DATETIME[, ENDDATETIME] [t/TAG]…​ [z/LINK] [r/INTERVAL RECURRENCE]` | +| [**List Tasks**](#listing-tasks--listt) | `listt all\` or `listt nc/` or `listt c/` | +| [**Edit Task**](#editing-a-task-editt) | `editt INDEX [tn/TASKNAME] [dt/DATETIME, ENDDATETIME*] [z/LINK] [t/TAG]` | +| [**Find Task by name or tag**](#1-finding-tasks-by-name-or-tag-findt) | `findt KEYWORD [MORE_KEYWORDS]…​` | +| [**Find Task by Date**](#2-finding-tasks-by-date-findt) | `findt dt/DATETIME1, DATETIME2` | +| [**Assign contact
To Task**](#assigning-a-contact-to-a-task-assign) | `assign INDEX p/CONTACTINDEX` | +| [**View contacts
Assigned to Task**](#viewing-contacts-assigned-to-a-task-view) | `view INDEX` | +| [**Unassign contact
From Task**](#unassigning-a-contact-from-a-task-unassign) | `unassign INDEX p/CONTACTINDEX` | +| [**Mark Task**](#marking-a-task-as-done-mark) | `mark INDEX` | +| [**Unmark Task**](#unmarking-a-task-as-not-done-unmark) | `unmark INDEX` | +| [**Delete Task**](#deleting-a-task-deletet) | `deletet INDEX` | +| [**Generating emails of all the contacts
Assigned to task**](#generating-the-emails-of-all-the-contacts-assigned-to-a-task-gen) | `gen INDEX` | +| [**Clear all data**](#clearing-all-data--clear) | `clear` | +| [**Import contacts**](#importing-contacts-from-a-data-file--import) | `import fp/FILEPATH` | +| [**Exit**](#exiting-the-program--exit) | `exit` | + + diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..ee417a1226b 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "NUS Classes" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2122S2-CS2103T-T12-4/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..0ec3e3e4bfa 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -127,7 +127,7 @@ a { .social-media-list &:hover { text-decoration: none; - .username { + .gitUsername { text-decoration: underline; } } @@ -288,7 +288,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "NUS Classes"; font-size: 32px; } } diff --git a/docs/diagrams/Activity Diagram/DeleteTaskOutcome.puml b/docs/diagrams/Activity Diagram/DeleteTaskOutcome.puml new file mode 100644 index 00000000000..2da64825c51 --- /dev/null +++ b/docs/diagrams/Activity Diagram/DeleteTaskOutcome.puml @@ -0,0 +1,9 @@ +@startuml +(*) --> "DeleteTask command executed" +if "Delete task successful" then + -left->[true] "Message display: Deleted Task: Index" + -> (*) +else + -right->[false] "throw CommandException" + -->(*) +@enduml diff --git a/docs/diagrams/Activity Diagram/EditTaskOutcome.puml b/docs/diagrams/Activity Diagram/EditTaskOutcome.puml new file mode 100644 index 00000000000..dc7fe184fd8 --- /dev/null +++ b/docs/diagrams/Activity Diagram/EditTaskOutcome.puml @@ -0,0 +1,9 @@ +@startuml +(*) --> "EditTask command executed" +if "Edit task successful" then + -left->[true] "Message display: Update Task: 'Index'" + -> (*) +else + -right->[false] "throw CommandException" + -->(*) +@enduml diff --git a/docs/diagrams/AddTaskSequenceDiagram.puml b/docs/diagrams/AddTaskSequenceDiagram.puml new file mode 100644 index 00000000000..7bb898c70e4 --- /dev/null +++ b/docs/diagrams/AddTaskSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":AddTaskCommandParser" as AddTaskCommandParser LOGIC_COLOR +participant ":AddTaskCommand" as AddTaskCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("addt 1...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("addt 1...") +activate AddressBookParser + +create AddTaskCommandParser +AddressBookParser -> AddTaskCommandParser : AddTaskCommandParser() +activate AddTaskCommandParser + +AddTaskCommandParser --> AddressBookParser +deactivate AddTaskCommandParser + +AddressBookParser -> AddTaskCommandParser : parse("...") +activate AddTaskCommandParser + +create AddTaskCommand +AddTaskCommandParser -> AddTaskCommand : AddTaskCommand(...) +activate AddTaskCommand + +AddTaskCommand --> AddTaskCommandParser +deactivate AddTaskCommand + +AddTaskCommandParser --> AddressBookParser +deactivate AddTaskCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddTaskCommandParser -[hidden]-> AddressBookParser +destroy AddTaskCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> AddTaskCommand : execute() +activate AddTaskCommand + +AddTaskCommand -> Model : addTask(...) +activate Model + +Model --> AddTaskCommand +deactivate Model + +create CommandResult +AddTaskCommand -> CommandResult : CommandResult(...) +activate CommandResult + +CommandResult --> AddTaskCommand +deactivate CommandResult + +AddTaskCommand --> LogicManager : result +deactivate AddTaskCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index ef81d18c337..bfef152a898 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -7,10 +7,10 @@ Participant ":Logic" as logic LOGIC_COLOR Participant ":Model" as model MODEL_COLOR Participant ":Storage" as storage STORAGE_COLOR -user -[USER_COLOR]> ui : "delete 1" +user -[USER_COLOR]> ui : "deletec 1" activate ui UI_COLOR -ui -[UI_COLOR]> logic : execute("delete 1") +ui -[UI_COLOR]> logic : execute("deletec 1") activate logic LOGIC_COLOR logic -[LOGIC_COLOR]> model : deletePerson(p) diff --git a/docs/diagrams/DeleteModelSequenceDiagram.puml b/docs/diagrams/DeleteModelSequenceDiagram.puml new file mode 100644 index 00000000000..2e898a943ff --- /dev/null +++ b/docs/diagrams/DeleteModelSequenceDiagram.puml @@ -0,0 +1,32 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":AddressBook" as AddressBook MODEL_COLOR +participant ":TaskList" as TaskList MODEL_COLOR +end box + + +[-> DeleteCommand : execute() +activate DeleteCommand + +DeleteCommand -> Model : deletePerson(target) +activate Model + +Model -> AddressBook : removePerson(target) +activate AddressBook + +AddressBook --> Model +deactivate AddressBook + +Model -> TaskList : removePerson(target) +activate TaskList + +TaskList --> Model +deactivate TaskList +@enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 1dc2311b245..4bce125f07e 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -13,10 +13,10 @@ box Model MODEL_COLOR_T1 participant ":Model" as Model MODEL_COLOR end box -[-> LogicManager : execute("delete 1") +[-> LogicManager : execute("deletec 1") activate LogicManager -LogicManager -> AddressBookParser : parseCommand("delete 1") +LogicManager -> AddressBookParser : parseCommand("deletec 1") activate AddressBookParser create DeleteCommandParser diff --git a/docs/diagrams/DeleteTaskSequenceDiagram.puml b/docs/diagrams/DeleteTaskSequenceDiagram.puml new file mode 100644 index 00000000000..29baa2f8cc5 --- /dev/null +++ b/docs/diagrams/DeleteTaskSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":DeleteTaskCommandParser" as DeleteTaskCommandParser LOGIC_COLOR +participant ":DeleteTaskCommand" as DeleteTaskCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("deletet 1") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("deletet 1") +activate AddressBookParser + +create DeleteTaskCommandParser +AddressBookParser -> DeleteTaskCommandParser : DeleteTaskCommandParser() +activate DeleteTaskCommandParser + +DeleteTaskCommandParser --> AddressBookParser +deactivate DeleteTaskCommandParser + +AddressBookParser -> DeleteTaskCommandParser : parse("1") +activate DeleteTaskCommandParser + +create DeleteTaskCommand +DeleteTaskCommandParser -> DeleteTaskCommand : DeleteTaskCommand(1) +activate DeleteTaskCommand + +DeleteTaskCommand --> DeleteTaskCommandParser +deactivate DeleteTaskCommand + +DeleteTaskCommandParser --> AddressBookParser +deactivate DeleteTaskCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +DeleteTaskCommandParser -[hidden]-> AddressBookParser +destroy DeleteTaskCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> DeleteTaskCommand : execute() +activate DeleteTaskCommand + +DeleteTaskCommand -> Model : deleteTask(1) +activate Model + +Model --> DeleteTaskCommand +deactivate Model + +create CommandResult +DeleteTaskCommand -> CommandResult : CommandResult(...) +activate CommandResult + +CommandResult --> DeleteTaskCommand +deactivate CommandResult + +DeleteTaskCommand --> LogicManager : result +deactivate DeleteTaskCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EditTaskSequenceDiagram.puml b/docs/diagrams/EditTaskSequenceDiagram.puml new file mode 100644 index 00000000000..d76423c917e --- /dev/null +++ b/docs/diagrams/EditTaskSequenceDiagram.puml @@ -0,0 +1,122 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":EditTaskCommandParser" as EditTaskCommandParser LOGIC_COLOR +participant "ParserUtil" as ParserUtil LOGIC_COLOR +participant "editTaskDescriptor:EditTaskDescriptor" as EditTaskDescriptor LOGIC_COLOR +participant ":EditTaskCommand" as EditTaskCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + + -> LogicManager : parseCommand("editt ...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("editt ...") +activate AddressBookParser + +create EditTaskCommandParser +AddressBookParser -> EditTaskCommandParser : EditTaskCommandParser() +activate EditTaskCommandParser + +EditTaskCommandParser --> AddressBookParser +deactivate EditTaskCommandParser + +AddressBookParser -> EditTaskCommandParser : parse(...) +activate EditTaskCommandParser + +EditTaskCommandParser -> ParserUtil : parseIndex(...) +activate ParserUtil + +ParserUtil --> EditTaskCommandParser : index +deactivate ParserUtil + +create EditTaskDescriptor +EditTaskCommandParser -> EditTaskDescriptor : EditTaskDescriptor() +activate EditTaskDescriptor + +EditTaskDescriptor --> EditTaskCommandParser +deactivate EditTaskDescriptor + +opt Task name present + EditTaskCommandParser -> EditTaskDescriptor : setName(...) + activate EditTaskDescriptor + EditTaskDescriptor --> EditTaskCommandParser + deactivate EditTaskDescriptor +end + +opt DateTime present + EditTaskCommandParser -> EditTaskDescriptor : setDate(...) + activate EditTaskDescriptor + EditTaskDescriptor --> EditTaskCommandParser + deactivate EditTaskDescriptor +end + +opt Tags present +EditTaskCommandParser -> ParserUtil : parseTags(...) +activate ParserUtil +ParserUtil --> EditTaskCommandParser : Tags +deactivate ParserUtil +EditTaskCommandParser -> EditTaskDescriptor : setTags(...) +activate EditTaskDescriptor +EditTaskDescriptor --> EditTaskCommandParser +deactivate EditTaskDescriptor +end + +opt Link present +EditTaskCommandParser -> ParserUtil : parseLink(...) +activate ParserUtil +ParserUtil --> EditTaskCommandParser : Link +deactivate ParserUtil +EditTaskCommandParser -> EditTaskDescriptor : setLink(...) +activate EditTaskDescriptor +EditTaskDescriptor --> EditTaskCommandParser +deactivate EditTaskDescriptor +end + +create EditTaskCommand +EditTaskCommandParser -> EditTaskCommand : EditTaskCommand(index, editTaskDescriptor) +activate EditTaskCommand + +EditTaskCommand --> EditTaskCommandParser +deactivate EditTaskCommand + +EditTaskCommandParser --> AddressBookParser +deactivate EditTaskCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditTaskCommandParser -[hidden]-> AddressBookParser +destroy EditTaskCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> EditTaskCommand : execute() +activate EditTaskCommand + +EditTaskCommand -> Model : setTask(...) +activate Model + +Model --> EditTaskCommand +EditTaskCommand -> Model : updateFilteredTaskList(...) +Model --> EditTaskCommand +deactivate Model + +create CommandResult +EditTaskCommand -> CommandResult : CommandResult(...) +activate CommandResult + +CommandResult --> EditTaskCommand +deactivate CommandResult + +EditTaskCommand --> LogicManager : result +deactivate EditTaskCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 4439108973a..d8abdbebaca 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -7,44 +7,57 @@ skinparam classBackgroundColor MODEL_COLOR Package Model <>{ Class "<>\nReadOnlyAddressBook" as ReadOnlyAddressBook Class "<>\nReadOnlyUserPrefs" as ReadOnlyUserPrefs +Class "<>\nReadOnlyTaskList" as ReadOnlyTaskList Class "<>\nModel" as Model Class AddressBook Class ModelManager Class UserPrefs +Class TaskList +Class Task Class UniquePersonList Class Person -Class Address +Class Username Class Email Class Name Class Phone Class Tag +Class Link } Class HiddenOutside #FFFFFF HiddenOutside ..> Model -AddressBook .up.|> ReadOnlyAddressBook +AddressBook .up..|> ReadOnlyAddressBook +TaskList .up..|> ReadOnlyTaskList ModelManager .up.|> Model -Model .right.> ReadOnlyUserPrefs -Model .left.> ReadOnlyAddressBook +Model .down.> ReadOnlyUserPrefs +Model .down.> ReadOnlyAddressBook +Model .down.> ReadOnlyTaskList ModelManager -left-> "1" AddressBook -ModelManager -right-> "1" UserPrefs +ModelManager -up-> "1" UserPrefs +ModelManager -right-> "1" TaskList UserPrefs .up.|> ReadOnlyUserPrefs +TaskList --> "~* " Task + AddressBook *--> "1" UniquePersonList -UniquePersonList --> "~* all" Person +UniquePersonList --> "~* " Person Person *--> Name Person *--> Phone Person *--> Email -Person *--> Address +Person *--> Username Person *--> "*" Tag +Task -> "*" Tag +Task -down-> "0 .. 1" Link + Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +Task -[hidden]left-----> Person + -ModelManager -->"~* filtered" Person +ModelManager -->"~*" Person +ModelManager -->"~*" Task @enduml diff --git a/docs/diagrams/ParserClasses.puml b/docs/diagrams/ParserClasses.puml index 0c7424de6e0..1d0a72ba1ef 100644 --- a/docs/diagrams/ParserClasses.puml +++ b/docs/diagrams/ParserClasses.puml @@ -35,4 +35,5 @@ XYZCommandParser ..> ParserUtil ParserUtil .down.> Prefix ArgumentTokenizer .down.> Prefix XYZCommand -up-|> Command +ArgumentMultimap ..> Prefix @enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index 760305e0e58..db3bbcb8a77 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -19,20 +19,30 @@ Class "<>\nAddressBookStorage" as AddressBookStorage Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson -Class JsonAdaptedTag } +package "TaskList Storage" #F4F6F6{ +Class "<>\nTaskListStorage" as TaskListStorage +Class JsonTaskListStorage +Class JsonSerializableTaskList +Class JsonAdaptedTask +} + +Class JsonAdaptedTag + } Class HiddenOutside #FFFFFF HiddenOutside ..> Storage StorageManager .up.|> Storage -StorageManager -up-> "1" UserPrefsStorage -StorageManager -up-> "1" AddressBookStorage +StorageManager -down-> "1" UserPrefsStorage +StorageManager -right-> "1" AddressBookStorage +StorageManager -left-> "1" TaskListStorage -Storage -left-|> UserPrefsStorage -Storage -right-|> AddressBookStorage +Storage -down-|> UserPrefsStorage +Storage -down-|> AddressBookStorage +Storage -down-|>TaskListStorage JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage @@ -40,4 +50,9 @@ JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonTaskListStorage .up.|> TaskListStorage +JsonTaskListStorage ..> JsonSerializableTaskList +JsonSerializableTaskList --> "*" JsonAdaptedTask +JsonAdaptedTask --> "*" JsonAdaptedTag + @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..54b187942c5 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -12,7 +12,9 @@ Class MainWindow Class HelpWindow Class ResultDisplay Class PersonListPanel +Class TaskListPanel Class PersonCard +Class TaskCard Class StatusBarFooter Class CommandBox } @@ -33,10 +35,12 @@ UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" TaskListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow PersonListPanel -down-> "*" PersonCard +TaskListPanel -down-> "*" TaskCard MainWindow -left-|> UiPart @@ -44,10 +48,13 @@ ResultDisplay --|> UiPart CommandBox --|> UiPart PersonListPanel --|> UiPart PersonCard --|> UiPart +TaskListPanel --|> UiPart +TaskCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart -PersonCard ..> Model +PersonCard ...> Model +TaskCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic diff --git a/docs/diagrams/ViewSequenceDiagram.puml b/docs/diagrams/ViewSequenceDiagram.puml new file mode 100644 index 00000000000..41798215a1c --- /dev/null +++ b/docs/diagrams/ViewSequenceDiagram.puml @@ -0,0 +1,80 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":ViewCommandParser" as ViewCommandParser LOGIC_COLOR +participant ":ViewCommand" as ViewCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":Task" as Task MODEL_COLOR + +end box + +[-> LogicManager : execute("view 1") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("view 1") +activate AddressBookParser + +create ViewCommandParser +AddressBookParser -> ViewCommandParser : ViewCommandParser() +activate ViewCommandParser + +ViewCommandParser --> AddressBookParser +deactivate ViewCommandParser + +AddressBookParser -> ViewCommandParser : parse("1") +activate ViewCommandParser + +create ViewCommand +ViewCommandParser -> ViewCommand : ViewCommand(1) +activate ViewCommand + +ViewCommand --> ViewCommandParser +deactivate ViewCommand + +ViewCommandParser --> AddressBookParser +deactivate ViewCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ViewCommandParser -[hidden]-> AddressBookParser +destroy ViewCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> ViewCommand : execute() +activate ViewCommand + +ViewCommand -> Model : getFilteredTaskList() +activate Model + +Model --> ViewCommand +deactivate Model + +ViewCommand -> Task : getPeople() +activate Task + +Task --> ViewCommand : x +deactivate Task + +ViewCommand -> Model : updateFilteredPersonList(x) + +create CommandResult +ViewCommand -> CommandResult : CommandResult(...) +activate CommandResult + +CommandResult --> ViewCommand +deactivate CommandResult + +ViewCommand --> LogicManager : result +deactivate ViewCommand + +[<--LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/images/Activity Diagram/DeleteTaskOutcome.png b/docs/images/Activity Diagram/DeleteTaskOutcome.png new file mode 100644 index 00000000000..189f6c4a9ad Binary files /dev/null and b/docs/images/Activity Diagram/DeleteTaskOutcome.png differ diff --git a/docs/images/Activity Diagram/EditTaskOutcome.png b/docs/images/Activity Diagram/EditTaskOutcome.png new file mode 100644 index 00000000000..b3698af2fdf Binary files /dev/null and b/docs/images/Activity Diagram/EditTaskOutcome.png differ diff --git a/docs/images/AddTaskSequenceDiagram.png b/docs/images/AddTaskSequenceDiagram.png new file mode 100644 index 00000000000..46eaeaa0c30 Binary files /dev/null and b/docs/images/AddTaskSequenceDiagram.png differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 2f1346869d0..162f7b22fd9 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/DeleteModelSequenceDiagram.png b/docs/images/DeleteModelSequenceDiagram.png new file mode 100644 index 00000000000..179c2a77709 Binary files /dev/null and b/docs/images/DeleteModelSequenceDiagram.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index fa327b39618..7850b7ff384 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/DeleteTaskSequenceDiagram.png b/docs/images/DeleteTaskSequenceDiagram.png new file mode 100644 index 00000000000..1fa917be5a5 Binary files /dev/null and b/docs/images/DeleteTaskSequenceDiagram.png differ diff --git a/docs/images/EditTaskDiagram/EditTaskSequenceDiagramState0.png b/docs/images/EditTaskDiagram/EditTaskSequenceDiagramState0.png new file mode 100644 index 00000000000..f9f6fdb1cbf Binary files /dev/null and b/docs/images/EditTaskDiagram/EditTaskSequenceDiagramState0.png differ diff --git a/docs/images/EditTaskDiagram/EditTaskSequenceDiagramState1.png b/docs/images/EditTaskDiagram/EditTaskSequenceDiagramState1.png new file mode 100644 index 00000000000..b906c2cbefc Binary files /dev/null and b/docs/images/EditTaskDiagram/EditTaskSequenceDiagramState1.png differ diff --git a/docs/images/EditTaskDiagram/EditTaskSequenceDiagramState2.png b/docs/images/EditTaskDiagram/EditTaskSequenceDiagramState2.png new file mode 100644 index 00000000000..2e722263432 Binary files /dev/null and b/docs/images/EditTaskDiagram/EditTaskSequenceDiagramState2.png differ diff --git a/docs/images/EditTaskSequenceDiagram.png b/docs/images/EditTaskSequenceDiagram.png new file mode 100644 index 00000000000..8d4857cca34 Binary files /dev/null and b/docs/images/EditTaskSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 04070af60d8..f4ad1760965 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png index e7b4c8880cd..2977bae6ce8 100644 Binary files a/docs/images/ParserClasses.png and b/docs/images/ParserClasses.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 2533a5c1af0..c48301d7563 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..74c67d1c4ba 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 785e04dbab4..6ae64810b7c 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/ViewSequenceDiagram.png b/docs/images/ViewSequenceDiagram.png new file mode 100644 index 00000000000..5dfedd83fd3 Binary files /dev/null and b/docs/images/ViewSequenceDiagram.png differ diff --git a/docs/images/adrianongjj.png b/docs/images/adrianongjj.png new file mode 100644 index 00000000000..3e280cb22a3 Binary files /dev/null and b/docs/images/adrianongjj.png differ diff --git a/docs/images/assignCommandShowcase.png b/docs/images/assignCommandShowcase.png new file mode 100644 index 00000000000..c76ea0bfcc5 Binary files /dev/null and b/docs/images/assignCommandShowcase.png differ diff --git a/docs/images/brian16600.png b/docs/images/brian16600.png new file mode 100644 index 00000000000..73c69d2dcf4 Binary files /dev/null and b/docs/images/brian16600.png differ diff --git a/docs/images/deletecCommandShowcase1.png b/docs/images/deletecCommandShowcase1.png new file mode 100644 index 00000000000..18eb1b52234 Binary files /dev/null and b/docs/images/deletecCommandShowcase1.png differ diff --git a/docs/images/deletecCommandShowcase2.png b/docs/images/deletecCommandShowcase2.png new file mode 100644 index 00000000000..4b9596bec9d Binary files /dev/null and b/docs/images/deletecCommandShowcase2.png differ diff --git a/docs/images/deletetCommandShowcase2.png b/docs/images/deletetCommandShowcase2.png new file mode 100644 index 00000000000..af1068b7806 Binary files /dev/null and b/docs/images/deletetCommandShowcase2.png differ diff --git a/docs/images/deletettCommandShowcase1.png b/docs/images/deletettCommandShowcase1.png new file mode 100644 index 00000000000..9c08e93cb29 Binary files /dev/null and b/docs/images/deletettCommandShowcase1.png differ diff --git a/docs/images/editcCommandShowcase.png b/docs/images/editcCommandShowcase.png new file mode 100644 index 00000000000..da207b17759 Binary files /dev/null and b/docs/images/editcCommandShowcase.png differ diff --git a/docs/images/edittCommandShowcase1.png b/docs/images/edittCommandShowcase1.png new file mode 100644 index 00000000000..31760208177 Binary files /dev/null and b/docs/images/edittCommandShowcase1.png differ diff --git a/docs/images/edittCommandShowcase2.png b/docs/images/edittCommandShowcase2.png new file mode 100644 index 00000000000..ea37467bee0 Binary files /dev/null and b/docs/images/edittCommandShowcase2.png differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png deleted file mode 100644 index 235da1c273e..00000000000 Binary files a/docs/images/findAlexDavidResult.png and /dev/null differ diff --git a/docs/images/findcCommandShowcase.png b/docs/images/findcCommandShowcase.png new file mode 100644 index 00000000000..371538ee9e7 Binary files /dev/null and b/docs/images/findcCommandShowcase.png differ diff --git a/docs/images/findtCommandShowcase.png b/docs/images/findtCommandShowcase.png new file mode 100644 index 00000000000..3b0b1f8f742 Binary files /dev/null and b/docs/images/findtCommandShowcase.png differ diff --git a/docs/images/genCommandShowcase.png b/docs/images/genCommandShowcase.png new file mode 100644 index 00000000000..bc6ebcacd43 Binary files /dev/null and b/docs/images/genCommandShowcase.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png deleted file mode 100644 index b1f70470137..00000000000 Binary files a/docs/images/helpMessage.png and /dev/null differ diff --git a/docs/images/helpmessage2.png b/docs/images/helpmessage2.png new file mode 100644 index 00000000000..dc09c773086 Binary files /dev/null and b/docs/images/helpmessage2.png differ diff --git a/docs/images/importCommandShowcase1.png b/docs/images/importCommandShowcase1.png new file mode 100644 index 00000000000..80bdfe487ba Binary files /dev/null and b/docs/images/importCommandShowcase1.png differ diff --git a/docs/images/importCommandShowcase2.png b/docs/images/importCommandShowcase2.png new file mode 100644 index 00000000000..d4a0271fa49 Binary files /dev/null and b/docs/images/importCommandShowcase2.png differ diff --git a/docs/images/junjunjieong.png b/docs/images/junjunjieong.png new file mode 100644 index 00000000000..5b6eac25fe3 Binary files /dev/null and b/docs/images/junjunjieong.png differ diff --git a/docs/images/junrong98.png b/docs/images/junrong98.png new file mode 100644 index 00000000000..dd9ae6a5ac4 Binary files /dev/null and b/docs/images/junrong98.png differ diff --git a/docs/images/marktCommandShowcase.png b/docs/images/marktCommandShowcase.png new file mode 100644 index 00000000000..b7c7a955bdf Binary files /dev/null and b/docs/images/marktCommandShowcase.png differ diff --git a/docs/images/snss231.png b/docs/images/snss231.png new file mode 100644 index 00000000000..edd9c201644 Binary files /dev/null and b/docs/images/snss231.png differ diff --git a/docs/images/unassignCommandShowcase.png b/docs/images/unassignCommandShowcase.png new file mode 100644 index 00000000000..7b6d9923b50 Binary files /dev/null and b/docs/images/unassignCommandShowcase.png differ diff --git a/docs/images/unmarkCommandShowcase.png b/docs/images/unmarkCommandShowcase.png new file mode 100644 index 00000000000..cd96b6c3383 Binary files /dev/null and b/docs/images/unmarkCommandShowcase.png differ diff --git a/docs/images/viewtCommandShowcase.png b/docs/images/viewtCommandShowcase.png new file mode 100644 index 00000000000..ead2ff01891 Binary files /dev/null and b/docs/images/viewtCommandShowcase.png differ diff --git a/docs/img.png b/docs/img.png new file mode 100644 index 00000000000..64590a54b57 Binary files /dev/null and b/docs/img.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..c28b2a2200d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,19 @@ --- layout: page -title: AddressBook Level-3 +title: NUS Classes --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![CI Status](https://github.com/ay2122s2-cs2103t-t12-4/tp/workflows/Java%20CI/badge.svg)](https://github.com/ay2122s2-cs2103t-t12-4/tp/actions) +[![codecov](https://codecov.io/gh/ay2122s2-cs2103t-t12-4/tp/branch/master/graph/badge.svg)](https://codecov.io/gh/ay2122s2-cs2103t-t12-4/tp) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). - -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +**NUS Classes is a desktop application for managing your contacts and tasks, with features such as marking tasks, assigning tasks to contacts and searching for tasks or contacts.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +* If you are interested in using NUS Classes, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing NUS Classes, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +a **Acknowledgements** * Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) diff --git a/docs/team/adrianongjj.md b/docs/team/adrianongjj.md new file mode 100644 index 00000000000..ef3e6aa797f --- /dev/null +++ b/docs/team/adrianongjj.md @@ -0,0 +1,52 @@ +--- +layout: page + +title: Ong John Jun, Adrian Project Portfolio Page +--- + +### Project: NUSClasses + +NUS Classes is an app that enables the professors to better manage contacts from large numbers of students and staff, and allow the professor to document the task that he/she needs to do. + +Given below are my contributions to the project. + +* **New Feature**: Finding tasks by date and/or time + * What it does: Find all tasks that are in between the given date time range + * Justification: Users can check their availability within a certain time frame + * Highlights: + * Uses same command as `findt` so the user can have 1 less command to remember + * Input order for date time search is not required, i.e. `findt dt/19-03-2022 1000, 19-03-2022 1200` is the same as `findt dt/19-03-2022 1200, 19-03-2022 1000` + * **Credits:** *Reused some functionalities from the existing AB3 to suit Task* + + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=AdrianOngJJ&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-02-18) + + +* **Project management**: + * Set up team's repo + * Assigning tasks to myself + +* **Enhancements to existing feature**: + * What it does: Find all tasks based on their tags + * Justification: Users can filter tasks based on tags + * **Credits:** *Help from [brain16600](https://github.com/brian16600)* + +* **Enhancements to existing feature**: + * What it does: Find all contacts based on their tags + * Justification: Users can filter contacts based on tags + * **Credits:** *Help from [brain16600](https://github.com/brian16600)* + +* **Documentation**: + * User Guide: + * Updated `findt` based on tags [\#190](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/190) + * Changed `filter` to `findt` for standardisation [\#186](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/186) + * Added `filter` command [\#64](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/64) + * Developer Guide: + * Added `findt` logic [\#303](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/303) + * Updated Glossary [\#22](../DeveloperGuide.md) + +* **Community**: + * Clarify my coding intention for one of my PR [#135](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/135) + +* **Tools**: + * IntelliJ: IDE diff --git a/docs/team/brian16600.md b/docs/team/brian16600.md new file mode 100644 index 00000000000..becc99a0841 --- /dev/null +++ b/docs/team/brian16600.md @@ -0,0 +1,122 @@ +--- +layout: page +title: Brian's Project Portfolio Page +--- + +### Project: NUS Classes + +NUS Classes is a desktop app for NUS Computing professors to manage their tasks and contacts. It includes task management features such as creating tasks, tagging tasks, assigning contacts to tasks, and marking tasks as complete or incomplete. It also includes contact management features such as finding contacts, assigning contacts to specific tasks and tagging contacts. + +Given below are my contributions to the project: + +* **New Feature:** Built the foundations for the Task functionality. + * **What it does:** Set up the framework for our NUS Classes product, which is a contact and task management app. By adding the task feature onto AB3's existing contact functionality, +this feature is the foundation for all Task-related commands in NUS Classes. Fields such as Task Name, Tag, DateTime set up the basic information contained within a Task, enabling NUS Computing professors to track their tasks + * **Highlights:** This addition affects existing commands and commands to be added in the future. It required an in-depth analysis of what information should be encapsulated in a Task. The implementation too was challenging as it had to be correctly implemented to ensure all future commands built on this foundation work. + * **Credits:** *Reused some functionalities from the existing AB3 to suit Task* +

+* **New Feature:** Added AddTaskCommand. (Pull Request [\#55](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/55), [\#78](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/78)) + * **What it does:** Adds a Task to NUS Classes with the relevant information. Users can add Tasks with the relevant information such as `TASKNAME`, `DATETIME` and `TAGS`. + * **Justification:** This allows NUS Computing Professors to keep track of their Task duties and stay organised. +

+* **New Feature:** Added AddTaskCommandParser. (Pull Request [\#78](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/78)) + * **What it does:** Parses information from the user with the `addt` parameter and extracts relevant information to create a Task. Relevant information such as `TASKNAME`, `DATETIME`, `TAGS` are all parsed accurately to ensure the user's input is relevant. Invalid inputs such as invalid datetime, missing parameters, or even invalid user inputs are reflected to the user. +This allows the user to easily create tasks with relevant information. + * **Highlights:** Custom error messages based on what the user has input incorrectly. For example for missing parameters, the error message will specify what parameters are missing. For invalid indexes, the error message will specify it as well, and invalid datetime is also covered. + * **Justification:** A parser specific to AddTaskCommand ensures relevant input is parsed and custom messages highlight to the user what is needed to be fixed. +

+* **New Feature:** Person includes their `GITHUB_USERNAME`. Updated UI accordingly. (Pull Request [\#98](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/98), [\#126](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/126)) + * **What it does:** Introduce Github Username functionality for contacts for NUS Computing professors to better manage their contacts. This also allows profs to easily review students' and TAs' work on Github. + * **Justification:** Github username is a relevant field for NUS Computing professors for many project modules, and it will make contact management easier. + * **Highlights:** Valid username inputs are in line with Github username standards, meaning that usernames can only contain alphanumeric characters or singular hyphens, and not starting with or ending with a hyphen. + * **Credits:** [Stack Overflow](https://stackoverflow.com/questions/58726546/github-username-convention-using-regex) for the regex for Github usernames. +

+* **New Feature:** Specific customised error messages for `addc`, `addt`, `assign`, `unassign`, `deletec`, `deletet`, `editc`, `editt`, `findc`, `gen`, `mark`, `unmark`, `view`. + * **What it does:** Give the user specific error messages informing them of what parameters are missing/what parameters are invalid. + * **Justification:** The user can more easily fix their command rather than combing through the User Guide. +

+* **New Feature:** Wrote tests for `AddTaskCommand`, `AddTaskCommandParser` and `GitUsername`. + * **Justification:** Proper testing to ensure these functions work. + + + +**Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=cs2103t-t12-4&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-02-18) + +**Project management**: + * Did project incrementally, with code contributions and commits to team repo in every week. + * Finished all tasks by version deadline. + * Regularly did PR reviews, issue tracker and assigning of issues based on team meetings. + * Managed releases `v1.2` - `v1.4` (3 releases) on GitHub. + * Did necessary team tasks. + + +**Enhancements to existing features**:
+ +**Update:** AddContact and EditCommand to take in the relevant input. (Pull Request ([\#88](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/88), [\#98](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/98), [\#183](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/183))) + * **What it does:** Update these functionalities to be more appropriate for NUS Computing professor: Removed `ADDRESS` field and added `GITHUB_USERNAME` field for relevancy. + * **Highlights:** Customised error messages for duplicated fields of `PHONENUMBER`, `EMAIL` and `USERNAME` as no two people should have these same two fields. + +**Update:** AddContactParser and EditCommandParser command to parse relevant details and throw custom error messages. (Pull Request ([\#88](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/88), [\#183](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/183))) + * **What it does:** Update these parsers parse `USERNAME` rather than `ADDRESS` and to throw custom error messages based on what parameters are missing. + * **Justification:** Custom error messages let the user know what needs to be changed. + +**Update:** Added new logo for NUS Classes. + +**Update:** Zoom Link display color. (Pull Request [\#153](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/153)) + +**Update:** SampleDataUtil for more relevant tasks for NUS Classes as default tasks/contacts. (Pull Request [\#153](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/153)) + +**Update:** Update tests for `AddCommand`, `AddCommandParser` and `EditCommand`. + +**Update:** Added tests for `TaskBetweenDatesPredicate` + +**Update:** Update test cases based on new error messages for all classes. + +**Update:** Update UniquePersonList from AB3's implementation. This allows the functionality of contacts to allow same name among contacts. + * **What it does:** Differentiate contacts via their email, phone and Github Username (which are required to be unique) + * **Justification:** Professors may have many contacts with the same name but are different students, especially for larger modules. + + +**Documentation**:
+* User Guide: + * Added images for examples of each command. (Pull Request [\#158](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/158)) + * Added images of UI. (Pull Request [\#158](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/158)) + * Added write-up for description of NUS Classes. + * Updated Table of Contents. + * Added documentation for the features `add contact` and `add task` + * Rewrote examples for existing features to be more appropriate to NUS Classes, e.g. `Lecture` as example of Tasks. + * Updated Command Summary in UG + * Fixed PE-D documentation issues (Pull Requests ([\#249](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/249))) + + +* README: + * Updated README.md + + +* SettingUp: + * Updated SettingUp.md + + +* Developer Guide: + * Added description of NUS Classes + * Fixed naming in DG + * Update Design Considerations in DG + * Update Use cases in DG + * Added Adding Task implementation in DG. + + +**Community**: + * PRs reviewed (with non-trivial review comments): [\#258](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/258), [\#253](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/253), +[\#175](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/175), [\#170](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/170), [\#166](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/166), +[\#162](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/162), [\#95](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/95) + * Maintained Issue Tracker and commented on Issues for others. + * Regularly reviewed PRs + + + +**Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo. + + * _{you can add/remove categories in the list above}_ + diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index 773a07794e2..00000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -layout: page -title: John Doe's Project Portfolio Page ---- - -### Project: AddressBook Level 3 - -AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. - -Given below are my contributions to the project. - -* **New Feature**: Added the ability to undo/redo previous commands. - * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. - * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. - * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. - * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* - -* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. - -* **Code contributed**: [RepoSense link]() - -* **Project management**: - * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub - -* **Enhancements to existing features**: - * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) - * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) - -* **Documentation**: - * User Guide: - * Added documentation for the features `delete` and `find` [\#72]() - * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() - * Developer Guide: - * Added implementation details of the `delete` feature. - -* **Community**: - * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() - * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) - * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) - * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) - -* **Tools**: - * Integrated a third party library (Natty) to the project ([\#42]()) - * Integrated a new Github plugin (CircleCI) to the team repo - -* _{you can add/remove categories in the list above}_ diff --git a/docs/team/junjunjieong.md b/docs/team/junjunjieong.md new file mode 100644 index 00000000000..b5a4e1eeb90 --- /dev/null +++ b/docs/team/junjunjieong.md @@ -0,0 +1,69 @@ +--- +layout: page + +title: Ong Jun Jie Project Portfolio Page +--- + +### Project: NUSClasses + +NUS Classes is a desktop app for NUS Computing professors to manage their tasks and contacts. It includes task management features such as +creating tasks, tagging tasks, assigning contacts to tasks, and marking tasks as complete or incomplete. It also includes contact management features such as finding contacts, assigning contacts to specific tasks and tagging contacts. + +Given below are my contributions to the project. + +* **New Feature**: Delete Task feature + * What it does: Delete the task from the task list based on index + * Justification: Important for user to remove the task that is not needed + * Credits: *{code ideas from AB3 delete contacts feature}* + +* **New Feature**: Edit Task feature + * What it does: Edit the selected task based on the field(s) user want to edit + * Justification: Important for user to edit the task to change or update the information + * Highlights: + * This enhancement requires in-depth analysis on the class `EditPersonDescriptor` and require to modify it to suit for `Task`. + * Able to construct `EditTaskDescriptor` and provide flexibility to modify it if there is any changes to the fields in class`Task` + * Credits: *{code ideas from AB3 edit contacts feature}* + +* **New Feature**: Mark and Unmark Task features + * What it does: + * Mark feature allows the user to set the task as done + * Unmark feature allows the user to set the task as not done + * Justification: User may want to know which task has been completed + * Highlights: This enhancement requires to understand the structure of `UI` and require to modify the `taskcard` UI and FMXL to hold image that represent the checkbox. + +* **New Feature**: List Task feature + * What it does: List the task based on the prefix(all/, c/, nc/) input by user. + * Justification: User may prefer to see all the available task only, or see list of completed/uncompleted task only. + * Credits: *{code ideas from AB3 list feature with modification}* + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=junjunjieOng&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-02-18) + +* **Project management**: + * Managed releases `v1.2` - `v1.4rc` (3 releases) on GitHub + +
+* **Documentation**: + * User Guide: + * Added documentation for the feature `deletet` [\#6](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/6) + * Added documentation for the features `editt` [\#86](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/86) + * Added documentation for the features `mark` and `unmark` [\#150](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/150) + * Added documentation for the features `listt` [\#154](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/154) + * Annotated the document images with [brain16600](https://github.com/brian16600) for clearer explanation [\#276](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/276) + * Developer Guide: + * Updated `user profile` and `value proposition` [\#7](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/7/files) + * Updated `User Stories` and `Non-functional requirement` [\#7](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/7/files) + * Added implementation detail of `deletet` and `editt` feature [\#107](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/107) + * Created activity diagram and Sequence diagram for `editt` and `deletet` feature [\#107](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/107) + +* **Community**: + * Total PRs reviewed (with non-trivial review comments): **14** + * [\#106](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/106), [\#114](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/114), + [\#145](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/145), [\#149](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/149), + [\#158](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/158), [\#175](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/175), + [\#183](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/183), [\#251](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/251), + [\#255](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/255), [\#260](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/260), + [\#263](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/263), [\#276](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/276), + [\#278](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/278), [\#281](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/281) + * Reported bugs and suggestions for other team in [PE-D](https://github.com/junjunjieOng/ped/issues) + + diff --git a/docs/team/junrong98.md b/docs/team/junrong98.md new file mode 100644 index 00000000000..cb398862517 --- /dev/null +++ b/docs/team/junrong98.md @@ -0,0 +1,69 @@ +--- +layout: page +title: Jun Rong's Project Portfolio Page +--- + +### Project: Nus Classes + +NUS Classes is an app that enables the professors to better manage contacts from large numbers of students and staff, and allow the professor to document the task that he/she needs to do. +Given below are my contributions to the project. + +* **New Feature**: View Task (Pull Request [\#68](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/68)) + * What it does: Allows user to view who were assigned to each task. + * Justification: This allows NUS Computing Professors to easily remember who they assigned to the task. + * Highlights: Understanding how `ObservableList` works and the effect it has on the GUI. + * Credits: *Reused code ideas from AB3* +

+* **New Feature**: Storing link in tasks (Pull Request [\#95](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/95)) + * What it does: Stores link in tasks. + * Justification: This allows NUS Computing Professors to store link to task for easier management. +

+ +* **New Feature**: Create recurring tasks (Pull Request [\#114](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/114)) + * What it does: Allows user to create a task repeatedly with a fixed interval using a single command. + * Justification: This allows NUS Computing Professors to create tasks that occur regularly conveniently. + * Credits: [baeldung](https://www.baeldung.com/java-initialize-hashmap) *for the mapping of common date intervals. Reused with slight modifications* +

+* **New Feature**: List tasks (Pull Request [\#146](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/146)) + * What it does: List all tasks. + * Justification: This allows NUS Computing Professors to see all the tasks they created, after they have manipulated the list. + * Credits: *Reused some code ideas from AB3 to suit Task* +

+* **New Feature**: Highlighting of tasks (Pull Request [\#149](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/149)) + * What it does: Highlight tasks with different colour depending on their state. + * Justification: This allows NUS Computing Professors to easily see which tasks are coming up or are already dued. +

+ +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=junrong98&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-02-18) +

+* **Project management**: + * Managed releases `v1.2` - `v1.4rc` (3 releases) on GitHub +

+* **Documentation**: + * User Guide: + * Added documentation for the features `Viewing contacts assigned to a task` [\#68](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/68) + * Update documentation for the features `Adding a task` [\#114](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/114/files) + * Update documentation for NUS Classes [\#123](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/123/files) + * Developer Guide: + * Added documentation for the features `View contacts assigned to a task` [\#118](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/118) + * ReadMe: + * Update ReadMe. +

+ +* **Community**: + * Total PRs reviewed (with non-trivial review comments): **25** + * [\#45](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/45), [\#66](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/66), + [\#77](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/77), [\#100](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/100), + [\#107](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/107), [\#109](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/109), + [\#112](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/112), [\#117](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/117), + [\#132](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/132), [\#135](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/135), + [\#132](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/132), [\#135](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/135), + [\#144](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/144), [\#150](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/150), + [\#154](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/154), [\#157](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/157), + [\#158](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/158), [\#164](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/178), + [\#170](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/170), [\#178](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/178), + [\#180](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/180), [\#183](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/183), + [\#185](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/185), [\#186](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/186), + [\#249](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/249) + + * Reported bugs and suggestions for other team in [PE-D](https://github.com/junrong98/ped/issues) diff --git a/docs/team/snss231.md b/docs/team/snss231.md new file mode 100644 index 00000000000..147af6d04cc --- /dev/null +++ b/docs/team/snss231.md @@ -0,0 +1,79 @@ +--- +layout: page +title: Sean's Project Portfolio Page +--- + +### Project: NUS Classes + +NUS Classes is an all-in-one task and contacts organizer for NUS Computing professors. It is written in Java. + +Given below are my contributions to the project. + +* **New feature**: Implement the model and storage parts to facilitate a `Task` entity + * What it does: Allow users to add their tasks to NUS Classes. + * Justification: Users can use the feature to be well organised with their tasks + + +* **New Feature**: Assign, unassign command + * What it does: Allow users to assign and unassign contacts from tasks + * Justification: Users can use the feature for bookkeeping purposes as well as to facilitate other contact-related commands. + * Highlights: Users can use the `gen` command to easily generate and copy all emails related to a task for ease of communication. + + +* **New Feature**: Generate emails command + * What it does: Users can use the `gen` command to easily generate and copy all emails related to a task, then paste into their preferred email client. + * Justification: Users have an easy way to contact all contacts related to a task in case urgent dissemination of information is required (e.g. in the event of unforeseen circumstances) + + +* **New Feature**: Date and time range for events + * What it does: Users can specify a stand and end date and time for tasks (as opposed to just one single deadline) to represent events that occur during a specific interval. + * Justification: Users can easily keep track of start and end timings for events. + + +* **New Feature**: User-friendly date and time display + * What it does: Instead of displaying all dates in full e.g.`13 May 2022, 3.00pm - 13 May 2022, 5.00pm`, dates are displayed relative to the current date + * Examples: + * `Today` for tasks that occur on the current calendar day, + * `Friday` for tasks that occur on the same week + * `13 Apr` (omitting the year for tasks that occur on the same calendar year) + * `Today, 3.00 pm - 5.00 pm` (not repeating the end date when it is the same as the start date) + * Justification: Dates are far more intuitive and easy to read for the user. + + +* **New Feature**: Import contacts from .csv + * What it does: Import contacts from a .csv file to create contacts in NUS Classes. + * Justification: It can be incredibly time-consuming and tedious to add all contacts manually. + * Highlights: Users can easily export contacts from a spreadsheet containing contacts and relevant headers to import the contacts into NUS Classes. + + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&since=2022-02-18&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=false&tabOpen=true&tabType=authorship&tabAuthor=snss231&tabRepo=AY2122S2-CS2103T-T12-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false) + + +* **Project management**: + * Managed all releases `v1.1` - `v1.4` (4 releases) on GitHub + + +* **Enhancements to existing features**: + * Updates and deletions to contacts are propagated to all tasks to ensure consistency in the model side of things. + * Limit tag length to 50 characters to avoid UI bugging out (part of the tag getting obscured) + * Polish GUI text feedback (e.g. message usages, fix typos and phrase messages better) + * Adjust name constraints to allow symbols that are commonly present in names (e.g. `Martin Luther King, Jr.`, `Joseph Lewitt-Hewman`, `Raj s/o Rajesh`) + * Change `clear` command to delete task data as well as contact data + + +* **Documentation**: + * User Guide: + * Added documentation for the features `clear`, `import` and `gen` + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `deletec` feature. + * Added design considerations for how tasks should be updated when contacts are deleted or updated. + * Updated text to reflect that the storage now handles both tasks and contacts + * Updated all UML class architecture diagrams to reflect the current status + * Readme.md, index.md + * Update links to CI and Codecov that was previously linking to AB-3 + + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#18](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/18), [\#59](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/59), [\#68](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/68), [\#83](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/83), [\#95](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/95), [\#98](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/98), [\#120](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/120), [\#121](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/121), [\#135](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/135), [\#170](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/170), [\#183](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/183), [\#249](https://github.com/AY2122S2-CS2103T-T12-4/tp/pull/249) + * Contributed to forum discussions (examples: [1](https://github.com/nus-cs2103-AY2122S2/forum/issues/241)) diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md index 4fb62a83ef6..809d8fb22bd 100644 --- a/docs/tutorials/TracingCode.md +++ b/docs/tutorials/TracingCode.md @@ -191,7 +191,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ ... Person personToEdit = lastShownList.get(index.getZeroBased()); Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + if (!personToEdit.isSamePerson(editedPerson) && model.hasName(editedPerson)) { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } model.setPerson(personToEdit, editedPerson); diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 4133aaa0151..bc3a4fef5d5 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -19,11 +19,14 @@ import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyTaskList; import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.TaskList; import seedu.address.model.UserPrefs; import seedu.address.model.util.SampleDataUtil; import seedu.address.storage.AddressBookStorage; import seedu.address.storage.JsonAddressBookStorage; +import seedu.address.storage.JsonTaskListStorage; import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; @@ -57,7 +60,8 @@ 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); + JsonTaskListStorage tasksStorage = new JsonTaskListStorage(userPrefs.getTaskListFilePath()); + storage = new StorageManager(addressBookStorage, userPrefsStorage, tasksStorage); initLogging(config); @@ -75,22 +79,40 @@ public void init() throws Exception { */ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { Optional addressBookOptional; - ReadOnlyAddressBook initialData; + ReadOnlyAddressBook initialAddressBookData; + Optional taskListOptional; + ReadOnlyTaskList initialTaskListData; try { addressBookOptional = storage.readAddressBook(); if (!addressBookOptional.isPresent()) { - logger.info("Data file not found. Will be starting with a sample AddressBook"); + logger.info("AddressBook data file not found. Will be starting with a sample AddressBook"); } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + initialAddressBookData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + + } catch (DataConversionException e) { logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + initialAddressBookData = new AddressBook(); } catch (IOException e) { logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + initialAddressBookData = new AddressBook(); } - return new ModelManager(initialData, userPrefs); + try { + taskListOptional = storage.readTaskList(); + if (!taskListOptional.isPresent()) { + logger.info("TaskList data file not found. Will be starting with a sample TaskList"); + } + initialTaskListData = taskListOptional.orElseGet(SampleDataUtil::getSampleTaskList); + + } catch (DataConversionException e) { + logger.warning("Data file not in the correct format. Will be starting with an empty TaskList"); + initialTaskListData = new TaskList(); + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with an empty TaskList"); + initialTaskListData = new TaskList(); + } + return new ModelManager(initialAddressBookData, userPrefs, initialTaskListData); } private void initLogging(Config config) { @@ -167,13 +189,13 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting NUS Classes " + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping NUS Classes ] ============================="); try { storage.saveUserPrefs(model.getUserPrefs()); } catch (IOException e) { diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e469..3c43381bbf9 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -5,9 +5,42 @@ */ public class Messages { - public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; + public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command! Use 'help' for a list of commands"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The contact index provided is invalid!" + + " Only a single non-zero positive integer is valid. Index must also not be out of bounds!\n" + + "Examples of invalid indexes: -1, 1 abc, 1 i/(invalid parameter)"; + public static final String MESSAGE_INVALID_RECURRENCE = "The recurrence parameter is invalid! \n%1$s"; + public static final String MESSAGE_INVALID_INTERVAL = "The interval parameter is invalid! \n%1$s"; + public static final String MESSAGE_INVALID_RECURRENCE_INDEX = "The interval/recurrence indexes are invalid!" + + " Only a single non-zero positive integer is valid. \n%1$s"; + public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d contacts listed!"; + public static final String MESSAGE_TASKS_LISTED_OVERVIEW = "%1$d tasks listed!"; + public static final String MESSAGE_INVALID_TASK_DISPLAYED_INDEX = "The task index provided is invalid!" + + " Only non-zero positive values are valid. Index must also not be out of bounds!\n" + + "Examples of invalid indexes: -1, 1 abc, 1 i/(invalid parameter)"; + public static final String MESSAGE_INVALID_DATE_RANGE = "The end date must be after the start date!"; + public static final String MESSAGE_NO_KEYWORDS = "You need to specify a keyword! \n%1$s"; + public static final String MESSAGE_INVALID_DATETIME = "The date and time provided is invalid!\n%1$s"; + public static final String MESSAGE_NEED_AT_LEAST_ONE_VALID_PARAMETER = + "You need at least one valid parameter!\n%1$s"; + public static final String MESSAGE_INVALID_PARAMETERS = "Missing/Invalid parameters: "; + public static final String MESSAGE_TAG_TOO_LONG = "Error: Tags can be at most 50 characters in length.\n" + + "The following tag(s) are too long:\n%s"; + + public static final String MESSAGE_DUPLICATE_GIT_USERNAME = "This Github username already exists in NUS Classes!\n" + + "Check again?"; + public static final String MESSAGE_DUPLICATE_EMAIL = "This email already exists in NUS Classes!\nCheck again?"; + public static final String MESSAGE_DUPLICATE_PHONE = "This phone number already exists in NUS Classes!\n" + + "Check again?"; + public static final String ERROR_MESSAGE_INVALID_FORMAT = + "Invalid date format. It should be \"dd-MM-yyyy HHmm\" or \"dd-MM-yyyy\"\n" + + "dd: Day (from 01 to 31); MM: Month (from 01 to 12); " + + "yyyy: Year (from 1 to 99999999); HH: Hour (from 00 to 23); " + + "mm: Minute (from 00 to 59)"; + + public static final String ERROR_MESSAGE_INVALID_PARAMETER = + "Invalid parameter format. It should be either \"dt/dd-MM-yyyy HHmm, dd-MM-yyyy HHmm\"" + + " or \"dt/dd-MM-yyyy, dd-MM-yyyy\""; } diff --git a/src/main/java/seedu/address/commons/util/TagUtil.java b/src/main/java/seedu/address/commons/util/TagUtil.java new file mode 100644 index 00000000000..f400b5de855 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/TagUtil.java @@ -0,0 +1,28 @@ +package seedu.address.commons.util; + +import static seedu.address.commons.core.Messages.MESSAGE_TAG_TOO_LONG; + +import java.util.Set; + +import seedu.address.model.tag.Tag; + +/** + * Static library of methods related to Tags + */ +public class TagUtil { + + /** + * Checks if any of the tags is too long (more than 50 characters). + * + * @return null if none of the tags are too long, error message otherwise. + */ + public static String checkTagLength(Set tags) { + if (tags.stream().anyMatch(tag -> tag.tagName.length() > 50)) { + return String.format(MESSAGE_TAG_TOO_LONG, + tags.stream() + .filter(tag -> tag.tagName.length() > 50) + .reduce("", (str, tag) -> tag + "\n" + str, (s1, s2) -> s1 + s2)); + } + return null; + } +} diff --git a/src/main/java/seedu/address/commons/util/TranslatorUtil.java b/src/main/java/seedu/address/commons/util/TranslatorUtil.java new file mode 100644 index 00000000000..6ac5b4d3d85 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/TranslatorUtil.java @@ -0,0 +1,28 @@ +package seedu.address.commons.util; + +import java.util.AbstractMap; +import java.util.Map; + +public class TranslatorUtil { + + //@@author + //Reused from https://www.baeldung.com/java-initialize-hashmap + // with minor modifications + private static final Map periodMap = Map.ofEntries( + new AbstractMap.SimpleEntry("annually", 365), + new AbstractMap.SimpleEntry("quarterly", 120), + new AbstractMap.SimpleEntry("monthly", 30), + new AbstractMap.SimpleEntry("weekly", 7), + new AbstractMap.SimpleEntry("daily", 1) + ); + //@@author + + /** + * Get the mapping of period to no. of days. + * + * @return Return the mapping of period to no. of days. + */ + public static Map getPeriodMapping() { + return periodMap; + } +} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..51426faf770 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -8,7 +8,9 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyTaskList; import seedu.address.model.person.Person; +import seedu.address.model.task.Task; /** * API of the Logic component @@ -33,6 +35,9 @@ public interface Logic { /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of tasks */ + ObservableList getFilteredTaskList(); + /** * Returns the user prefs' address book file path. */ @@ -44,7 +49,14 @@ public interface Logic { GuiSettings getGuiSettings(); /** - * Set the user prefs' GUI settings. + * Sets the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); + + /** + * Returns the Task List. + * + * @see seedu.address.model.Model#getTaskList() + */ + ReadOnlyTaskList getTaskList(); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9d9c6d15bdc..9be09d23332 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -14,7 +14,9 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyTaskList; import seedu.address.model.person.Person; +import seedu.address.model.task.Task; import seedu.address.storage.Storage; /** @@ -47,6 +49,7 @@ public CommandResult execute(String commandText) throws CommandException, ParseE try { storage.saveAddressBook(model.getAddressBook()); + storage.saveTaskList(model.getTaskList()); } catch (IOException ioe) { throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); } @@ -64,6 +67,11 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getFilteredTaskList() { + return model.getFilteredTaskList(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); @@ -78,4 +86,9 @@ public GuiSettings getGuiSettings() { public void setGuiSettings(GuiSettings guiSettings) { model.setGuiSettings(guiSettings); } + + @Override + public ReadOnlyTaskList getTaskList() { + return model.getTaskList(); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 71656d7c5c8..9b302296bb4 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -1,12 +1,16 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.commons.core.Messages.MESSAGE_DUPLICATE_EMAIL; +import static seedu.address.commons.core.Messages.MESSAGE_DUPLICATE_GIT_USERNAME; +import static seedu.address.commons.core.Messages.MESSAGE_DUPLICATE_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GIT_USERNAME; 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 seedu.address.commons.util.TagUtil; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.person.Person; @@ -16,30 +20,31 @@ */ public class AddCommand extends Command { - public static final String COMMAND_WORD = "add"; + public static final String COMMAND_WORD = "addc"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to NUS Classes.\n" + + "Usage: " + + COMMAND_WORD + " " + PREFIX_NAME + "NAME " + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_GIT_USERNAME + "GITHUB_USERNAME " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "98765432 " + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " + + PREFIX_GIT_USERNAME + "john123 " + PREFIX_TAG + "friends " + PREFIX_TAG + "owesMoney"; public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + private final Person toAdd; /** - * Creates an AddCommand to add the specified {@code Person} + * Constructs an AddCommand to add the specified {@code Person} */ public AddCommand(Person person) { requireNonNull(person); @@ -50,8 +55,23 @@ public AddCommand(Person person) { public CommandResult execute(Model model) throws CommandException { requireNonNull(model); - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + if (model.hasPhone(toAdd.getPhone())) { + throw new CommandException(MESSAGE_DUPLICATE_PHONE); + } + + if (model.hasEmail(toAdd.getEmail())) { + throw new CommandException(MESSAGE_DUPLICATE_EMAIL); + } + + if (model.hasUsername(toAdd.getUsername())) { + throw new CommandException(MESSAGE_DUPLICATE_GIT_USERNAME); + } + + String checkTagLength = TagUtil.checkTagLength(toAdd.getTags()); + + //null value represents no tags are too long. + if (checkTagLength != null) { + throw new CommandException(checkTagLength); } model.addPerson(toAdd); 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..58ba3e4de02 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddTaskCommand.java @@ -0,0 +1,150 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.isNull; +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_DATE_RANGE; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATETIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_RECURRING; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TASKNAME; + +import java.time.LocalDateTime; +import java.util.Set; + +import seedu.address.commons.util.TagUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.tag.Tag; +import seedu.address.model.task.Link; +import seedu.address.model.task.Task; + +/** + * Represents an AddTaskCommand. + */ +public class AddTaskCommand extends Command { + + /* Message printed if wrong usage */ + public static final String COMMAND_WORD = "addt"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a task to NUS Classes.\n" + + "Usage: " + + COMMAND_WORD + " " + + PREFIX_TASKNAME + "TASKNAME " + + PREFIX_DATETIME + "DATETIME [, ENDDATETIME]" + + " [" + PREFIX_TAG + "TAG] " + + " [" + PREFIX_LINK + "LINK] " + + " [" + PREFIX_RECURRING + "PERIOD RECURRENCE]\n" + + "Example: " + "addt" + " " + + PREFIX_TASKNAME + "Lecture " + + PREFIX_DATETIME + "25-12-2023 1800," + + "25-12-2023 2000 " + + PREFIX_TAG + "CS2103T " + + PREFIX_LINK + "https://... " + + PREFIX_RECURRING + "5 5\n" + + "Hint: for " + PREFIX_RECURRING + " you can use predefined values [annually, quarterly, monthly," + + " weekly, daily] for the period field."; + + public static final String ADD_TASK_SUCCESS = "Task added!"; + + private final String taskName; + private final LocalDateTime dateTime; + private final LocalDateTime endDateTime; + private final Set tags; + private final Link link; + private final int recurrence; + private final int period; + private final boolean isTaskMarkDone; + + + + /** + * Constructor for AddTaskCommand. Takes in 6 parameters, taskName, dateTime, tags, + * link, recurrence, and period. There can be multiple tags. + * + * @param taskName Name of Task. + * @param dateTime LocalDateTime object to represent date time of Task. + * @param tags A set of tags link to the Task. + * @param link Link of a task. + * @param recurrence The number of times the task should recur. + * @param period The number of days apart each task should be. + */ + public AddTaskCommand(String taskName, LocalDateTime dateTime, LocalDateTime endDateTime, Set tags, Link link, + int recurrence, int period) { + + requireAllNonNull(taskName, dateTime, tags); + this.taskName = taskName; + this.dateTime = dateTime; + this.endDateTime = endDateTime; + this.tags = tags; + this.link = link; + this.recurrence = recurrence; + this.period = period; + this.isTaskMarkDone = false; + } + + /** + * Constructor for AddTaskCommand without period or recurrence. + */ + public AddTaskCommand(String taskName, LocalDateTime dateTime, LocalDateTime endDateTime, Set tags, + Link link) { + this(taskName, dateTime, endDateTime, tags, link, 0, 0); + } + + /** + * Constructor that takes in a Task and creates an AddTaskCommand. + * + * @param task Task from which information is added to AddTaskCommand. + */ + public AddTaskCommand(Task task) { + this.taskName = task.getName(); + this.dateTime = task.getDateTime(); + this.endDateTime = task.getEndDateTime(); + this.tags = task.getTags(); + this.link = task.getLink(); + this.recurrence = 0; + this.period = 0; + this.isTaskMarkDone = false; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + String checkTagLength = TagUtil.checkTagLength(tags); + + //null value represents no tags are too long. + if (checkTagLength != null) { + throw new CommandException(checkTagLength); + } + Task taskToBeAdded = new Task(taskName, dateTime, endDateTime, tags, link, isTaskMarkDone); + + if (taskToBeAdded.hasInvalidDateRange()) { + throw new CommandException(MESSAGE_INVALID_DATE_RANGE); + } + model.addTask(taskToBeAdded); + for (int i = 1; i < period; i++) { + LocalDateTime temp = dateTime.plusDays(i * recurrence); + LocalDateTime tempEnd = null; + if (!isNull(endDateTime)) { + tempEnd = endDateTime.plusDays(i * recurrence); + } + taskToBeAdded = new Task(taskName, temp, tempEnd, tags, link, isTaskMarkDone); + model.addTask(taskToBeAdded); + } + return new CommandResult(ADD_TASK_SUCCESS); + } + + @Override + public boolean equals(Object other) { + return (other instanceof AddTaskCommand + && this.taskName.equals(((AddTaskCommand) other).taskName) + && this.dateTime.equals(((AddTaskCommand) other).dateTime) + && this.tags.equals(((AddTaskCommand) other).tags)); + } + + @Override + public String toString() { + return this.taskName + " " + this.dateTime + " " + this.tags + " " + this.link; + } +} diff --git a/src/main/java/seedu/address/logic/commands/AssignCommand.java b/src/main/java/seedu/address/logic/commands/AssignCommand.java new file mode 100644 index 00000000000..7eee7f281fd --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AssignCommand.java @@ -0,0 +1,120 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.task.Task; + +/** + * Assigns a contact in the contact list to a task. + */ +public class AssignCommand extends Command { + + public static final String COMMAND_WORD = "assign"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Add the person identified by the index number " + + "used in the displayed person list " + + "to the task identified by the index number used in the displayed task list.\n" + + "Usage: " + + COMMAND_WORD + " " + + "TASK_INDEX " + PREFIX_PERSON + "PERSON_INDEX\n" + + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PERSON + "2"; + + + public static final String MESSAGE_ADD_PERSON_TO_TASK_SUCCESS_MULTIPLE = + "Added %1$s, Number: %2$s to the task: `%3$s`\nThere are currently %4$s people assigned to this task."; + + public static final String MESSAGE_ADD_PERSON_TO_TASK_SUCCESS_SINGLE = + "Added %1$s, Number: %2$s to the task: `%3$s`\nThere is currently %4$s person assigned to this task."; + + public static final String MESSAGE_DUPLICATE_PERSON = + "Failed: The person %1$s is already assigned to the task: `%2$s`"; + + private final Index taskIndex; + private final Index personIndex; + + /** + * Constructs AssignCommand that takes in a task index and person index. + * + * @param taskIndex of the task in the filtered task list to be added to + * @param personIndex of the person in the filtered person list to add + */ + public AssignCommand(Index taskIndex, Index personIndex) { + requireNonNull(taskIndex); + requireNonNull(personIndex); + + this.personIndex = personIndex; + this.taskIndex = taskIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List filteredPersonList = model.getFilteredPersonList(); + List filteredTaskList = model.getFilteredTaskList(); + + if (personIndex.getZeroBased() >= filteredPersonList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + if (taskIndex.getZeroBased() >= filteredTaskList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + Task taskToEdit = filteredTaskList.get(taskIndex.getZeroBased()); + Person personToAdd = filteredPersonList.get(personIndex.getZeroBased()); + + if (taskToEdit.getPeople().contains(personToAdd)) { + throw new CommandException( + String.format(MESSAGE_DUPLICATE_PERSON, personToAdd.getName(), taskToEdit)); + } + Task updatedTask = getUpdatedTask(personToAdd, taskToEdit); + + model.setTask(taskToEdit, updatedTask); + + int numberOfPeople = updatedTask.getNoOfPeople(); + + if (numberOfPeople <= 1) { + return new CommandResult( + String.format(MESSAGE_ADD_PERSON_TO_TASK_SUCCESS_SINGLE, + personToAdd.getName(), personToAdd.getPhone(), updatedTask, numberOfPeople)); + } + + return new CommandResult( + String.format(MESSAGE_ADD_PERSON_TO_TASK_SUCCESS_MULTIPLE, + personToAdd.getName(), personToAdd.getPhone(), updatedTask, numberOfPeople)); + + } + + + /** + * Obtains the updated task. + * + * @param personToAdd Person object to be added. + * @param taskToUpdate Task to be changed. + * @return New edited Task. + * @throws CommandException If command format is wrong. + */ + private Task getUpdatedTask(Person personToAdd, Task taskToUpdate) { + List updatedList = new ArrayList<>(taskToUpdate.getPeople()); + updatedList.add(personToAdd); + Task editedTask = new Task(taskToUpdate.getName(), taskToUpdate.getDateTime(), taskToUpdate.getEndDateTime(), + updatedList, taskToUpdate.getTags(), taskToUpdate.getLink(), taskToUpdate.isTaskMark()); + return editedTask; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof AssignCommand + && personIndex.equals(((AssignCommand) other).personIndex) + && taskIndex.equals(((AssignCommand) other).taskIndex)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..372d82caf0f 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -4,6 +4,7 @@ import seedu.address.model.AddressBook; import seedu.address.model.Model; +import seedu.address.model.TaskList; /** * Clears the address book. @@ -11,13 +12,14 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_SUCCESS = "NUS Classes data has been cleared!"; @Override public CommandResult execute(Model model) { requireNonNull(model); model.setAddressBook(new AddressBook()); + model.setTaskList(new TaskList()); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 92f900b7916..c81ca878250 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -11,6 +11,8 @@ public class CommandResult { private final String feedbackToUser; + private final String emails; + /** Help information should be shown to the user. */ private final boolean showHelp; @@ -24,6 +26,7 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.emails = null; } /** @@ -34,10 +37,25 @@ public CommandResult(String feedbackToUser) { this(feedbackToUser, false, false); } + /** + * + * @return + */ + public CommandResult(String feedbackToUser, String emails) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.showHelp = false; + this.exit = false; + this.emails = requireNonNull(emails); + } + public String getFeedbackToUser() { return feedbackToUser; } + public String getEmails() { + return emails; + } + public boolean isShowHelp() { return showHelp; } @@ -46,6 +64,10 @@ public boolean isExit() { return exit; } + public boolean isGenerateEmails() { + return this.emails != null; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -60,12 +82,17 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && exit == otherCommandResult.exit + && emails == otherCommandResult.emails; } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, showHelp, exit, emails); + } + + public String toString() { + return getFeedbackToUser(); } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 02fd256acba..ebc6eefe992 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -15,17 +15,24 @@ */ public class DeleteCommand extends Command { - public static final String COMMAND_WORD = "delete"; + public static final String COMMAND_WORD = "deletec"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" + + "Index must be a positive integer.\n" + + "Usage: " + + COMMAND_WORD + " " + + "INDEX\n" + "Example: " + COMMAND_WORD + " 1"; public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; private final Index targetIndex; + /** + * Constructs a DeleteCommand to delete the specified {@code targetIndex} + * + */ public DeleteCommand(Index targetIndex) { this.targetIndex = targetIndex; } 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..cf2bf148abb --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteTaskCommand.java @@ -0,0 +1,59 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.task.Task; + +/** + * Deletes a task identified using it's displayed index. + */ +public class DeleteTaskCommand extends Command { + + public static final String COMMAND_WORD = "deletet"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the task identified by the index number used in the displayed task list.\n" + + "Usage: " + + COMMAND_WORD + " " + + "INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_TASK_SUCCESS = "Deleted Task: %1$s"; + + private final Index targetIndex; + + /** + * Constructs a DeleteTaskCommand to delete the specified {@code targetIndex} + * + */ + public DeleteTaskCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredTaskList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + Task taskToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteTask(taskToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_TASK_SUCCESS, taskToDelete)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteTaskCommand // instanceof handles nulls + && targetIndex.equals(((DeleteTaskCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 7e36114902f..464cfa7af37 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -1,8 +1,11 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.commons.core.Messages.MESSAGE_DUPLICATE_EMAIL; +import static seedu.address.commons.core.Messages.MESSAGE_DUPLICATE_GIT_USERNAME; +import static seedu.address.commons.core.Messages.MESSAGE_DUPLICATE_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GIT_USERNAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -17,10 +20,11 @@ import seedu.address.commons.core.Messages; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.TagUtil; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; -import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.GitUsername; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -31,29 +35,34 @@ */ public class EditCommand extends Command { - public static final String COMMAND_WORD = "edit"; + public static final String COMMAND_WORD = "editc"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " + + "\nExisting values will be overwritten by the input values. Index must be positive integers.\n" + + "Usage: " + + COMMAND_WORD + " " + + "INDEX " + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_GIT_USERNAME + "GITHUB_USERNAME] " + + "[" + PREFIX_TAG + "TAG]...\n " + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; + + PREFIX_EMAIL + "johndoe@example.com " + + PREFIX_GIT_USERNAME + "john123"; 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_NOT_EDITED = "At least one field to edit must be provided.\n" + + MESSAGE_USAGE; + public static final String MESSAGE_PERSON_NOT_EDITED = "At least one field of this contact must be edited! \n%1$s"; private final Index index; private final EditPersonDescriptor editPersonDescriptor; /** + * Constructs EditCommand that takes in a person index and editPersonDescriptor. * @param index of the person in the filtered person list to edit * @param editPersonDescriptor details to edit the person with */ @@ -77,8 +86,31 @@ public CommandResult execute(Model model) throws CommandException { Person personToEdit = lastShownList.get(index.getZeroBased()); Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + String checkTagLength = TagUtil.checkTagLength(editedPerson.getTags()); + + //null value represents no tags are too long. + if (checkTagLength != null) { + throw new CommandException(checkTagLength); + } + + if (model.hasPhone(editedPerson.getPhone()) + && !editedPerson.getPhone().equals(personToEdit.getPhone())) { + throw new CommandException(MESSAGE_DUPLICATE_PHONE); + } + + if (model.hasEmail(editedPerson.getEmail()) + && !editedPerson.getEmail().equals(personToEdit.getEmail())) { + throw new CommandException(MESSAGE_DUPLICATE_EMAIL); + } + + if (model.hasUsername(editedPerson.getUsername()) + && !editedPerson.getUsername().equals(personToEdit.getUsername())) { + throw new CommandException(MESSAGE_DUPLICATE_GIT_USERNAME); + } + + if (model.hasPerson(editedPerson) && editedPerson.getName().equals(personToEdit.getName())) { + + throw new CommandException(String.format(MESSAGE_PERSON_NOT_EDITED, MESSAGE_USAGE)); } model.setPerson(personToEdit, editedPerson); @@ -96,10 +128,10 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + GitUsername updatedGitUsername = editPersonDescriptor.getUsername().orElse(personToEdit.getUsername()); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(updatedName, updatedPhone, updatedEmail, updatedGitUsername, updatedTags); } @Override @@ -128,7 +160,7 @@ public static class EditPersonDescriptor { private Name name; private Phone phone; private Email email; - private Address address; + private GitUsername gitUsername; private Set tags; public EditPersonDescriptor() {} @@ -141,7 +173,7 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setName(toCopy.name); setPhone(toCopy.phone); setEmail(toCopy.email); - setAddress(toCopy.address); + setUsername(toCopy.gitUsername); setTags(toCopy.tags); } @@ -149,7 +181,7 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, gitUsername, tags); } public void setName(Name name) { @@ -160,6 +192,7 @@ public Optional getName() { return Optional.ofNullable(name); } + public void setPhone(Phone phone) { this.phone = phone; } @@ -176,12 +209,12 @@ public Optional getEmail() { return Optional.ofNullable(email); } - public void setAddress(Address address) { - this.address = address; + public void setUsername(GitUsername gitUsername) { + this.gitUsername = gitUsername; } - public Optional
getAddress() { - return Optional.ofNullable(address); + public Optional getUsername() { + return Optional.ofNullable(gitUsername); } /** @@ -219,7 +252,7 @@ public boolean equals(Object other) { return getName().equals(e.getName()) && getPhone().equals(e.getPhone()) && getEmail().equals(e.getEmail()) - && getAddress().equals(e.getAddress()) + && getUsername().equals(e.getUsername()) && getTags().equals(e.getTags()); } } diff --git a/src/main/java/seedu/address/logic/commands/EditTaskCommand.java b/src/main/java/seedu/address/logic/commands/EditTaskCommand.java new file mode 100644 index 00000000000..ab506ba9a3e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditTaskCommand.java @@ -0,0 +1,240 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_DATE_RANGE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATETIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TASKNAME; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_TASKS; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.TagUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.tag.Tag; +import seedu.address.model.task.Link; +import seedu.address.model.task.Task; + +/** + * Edits the details of an existing task in the task list. + */ +public class EditTaskCommand extends Command { + + public static final String COMMAND_WORD = "editt"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edit and update the details of the task identified" + + " by the index number used in the displayed task list. \n" + + "Existing values will be overwritten by the input values. Index must be a positive integer\n" + + "Usage: " + + COMMAND_WORD + " " + + "INDEX " + + "[" + PREFIX_TASKNAME + "TASK NAME] " + + "[" + PREFIX_DATETIME + "DATETIME(dd-mm-yyyy hhmm)] " + + "[" + PREFIX_LINK + "LINK] " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_TASKNAME + "CS2103T Lecture " + + PREFIX_DATETIME + "12-03-2023 1330 " + + PREFIX_LINK + "https://... " + + PREFIX_TAG + "Lecture"; + + public static final String MESSAGE_EDIT_TASK_SUCCESS = "Updated Task: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided.\n%1$s"; + public static final String MESSAGE_DUPLICATE_TASK = "This task already exists in your task list."; + + private final Index index; + private final EditTaskDescriptor editTaskDescriptor; + + /** + * Constructs EditTaskCommand that takes in a task index and editTaskDescriptor. + * @param index of the task in the filtered task list to edit + * @param editTaskDescriptor details to edit the task with + */ + public EditTaskCommand(Index index, EditTaskDescriptor editTaskDescriptor) { + requireNonNull(index); + requireNonNull(editTaskDescriptor); + + this.index = index; + this.editTaskDescriptor = new EditTaskDescriptor(editTaskDescriptor); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredTaskList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + Task taskToEdit = lastShownList.get(index.getZeroBased()); + Task editedTask = createEditedTask(taskToEdit, editTaskDescriptor); + + String checkTagLength = TagUtil.checkTagLength(editedTask.getTags()); + + //null value represents no tags are too long. + if (checkTagLength != null) { + throw new CommandException(checkTagLength); + } + + if (!taskToEdit.isSameTask(editedTask) && model.hasTask(editedTask)) { + throw new CommandException(MESSAGE_DUPLICATE_TASK); + } + + if (editedTask.hasInvalidDateRange()) { + throw new CommandException(MESSAGE_INVALID_DATE_RANGE); + } + + model.setTask(taskToEdit, editedTask); + model.updateFilteredTaskList(PREDICATE_SHOW_ALL_TASKS); + return new CommandResult(String.format(MESSAGE_EDIT_TASK_SUCCESS, editedTask)); + } + + /** + * Creates and returns a {@code Task} with the details of {@code taskToEdit} + * edited with {@code editTaskDescriptor}. + */ + private static Task createEditedTask(Task taskToEdit, EditTaskDescriptor editTaskDescriptor) { + requireNonNull(taskToEdit); + requireNonNull(editTaskDescriptor); + + String editName = editTaskDescriptor.getName().orElse(taskToEdit.getName()); + LocalDateTime editDate = editTaskDescriptor.getDate().orElse(taskToEdit.getDateTime()); + LocalDateTime editEndDate = editTaskDescriptor.getEndDate(); + Set editTag = editTaskDescriptor.getTags().orElse(taskToEdit.getTags()); + Link link = editTaskDescriptor.getLink().orElse(taskToEdit.getLink()); + boolean isTaskMarkDone = taskToEdit.isTaskMark(); + + return new Task(editName, editDate, editEndDate, editTag, link, isTaskMarkDone); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditTaskCommand)) { + return false; + } + + // state check + EditTaskCommand e = (EditTaskCommand) other; + return index.equals(e.index) + && editTaskDescriptor.equals(e.editTaskDescriptor); + } + + /** + * Stores the details to edit the task with. Each non-empty field value will replace the + * corresponding field value of the task. + */ + public static class EditTaskDescriptor { + private String name; + private LocalDateTime dateTime; + private LocalDateTime endDateTime; + private Set tags; + private Link link; + + public EditTaskDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditTaskDescriptor(EditTaskDescriptor toCopy) { + requireNonNull(toCopy); + setName(toCopy.name); + setDate(toCopy.dateTime); + setEndDate(toCopy.endDateTime); + setTags(toCopy.tags); + setLink(toCopy.link); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, dateTime, tags, link); + } + + public void setName(String name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setDate(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + public void setEndDate(LocalDateTime endDateTime) { + this.endDateTime = endDateTime; + } + + public void setLink(Link link) { + this.link = link; + } + + public Optional getDate() { + return Optional.ofNullable(dateTime); + } + + public LocalDateTime getEndDate() { + return endDateTime; + } + + public Optional getLink() { + return Optional.ofNullable(link); + } + + /** + * Sets {@code tag} to this object's {@code tag}. + * 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, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Optional> getTags() { + return Optional.ofNullable(tags); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditTaskDescriptor)) { + return false; + } + + // state check + EditTaskDescriptor e = (EditTaskDescriptor) other; + + return getName().equals(e.getName()) + && getDate().equals(e.getDate()) + && getTags().equals(e.getTags()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..eba2b6f8dcf 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -9,7 +9,7 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting NUS CLasses as requested ..."; @Override public CommandResult execute(Model model) { diff --git a/src/main/java/seedu/address/logic/commands/FilterByDateCommand.java b/src/main/java/seedu/address/logic/commands/FilterByDateCommand.java new file mode 100644 index 00000000000..51338dc74c9 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FilterByDateCommand.java @@ -0,0 +1,49 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.model.Model; +import seedu.address.model.task.TaskBetweenDatesPredicate; + + + +/** + * Finds and lists all tasks in task storage whose date falls in between two given dates. + * Date format: dd-MM-yyyy HHmm + */ +public class FilterByDateCommand extends Command { + + public static final String COMMAND_WORD = "findt"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all tasks whose dates fall within " + + "the specified dates inputs (order insensitive) and displays them as a list with index numbers.\n" + + "Parameters: dt/DATETIME1, DATETIME2\n" + + "Example: " + COMMAND_WORD + " dt/12-01-2022 0900, 13-02-2022 0900"; + + private final TaskBetweenDatesPredicate predicate; + + /** + * Constructor for FilterByDateCommand + * + * @param predicate Predicate that returns true if task's date falls on or in between the given range + */ + public FilterByDateCommand(TaskBetweenDatesPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredTaskList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_TASKS_LISTED_OVERVIEW, model.getFilteredTaskList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FilterByDateCommand // instanceof handles nulls + && predicate.equals(((FilterByDateCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index d6b19b0a0de..acb94709d54 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -8,15 +8,17 @@ /** * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. + * Keyword matching is case-insensitive. */ public class FindCommand extends Command { - public static final String COMMAND_WORD = "find"; + public static final String COMMAND_WORD = "findc"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names/tags contain any of " + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Usage: " + + COMMAND_WORD + " " + + "KEYWORD [MORE_KEYWORDS]...[TAG]...\n" + "Example: " + COMMAND_WORD + " alice bob charlie"; private final NameContainsKeywordsPredicate predicate; diff --git a/src/main/java/seedu/address/logic/commands/FindTaskCommand.java b/src/main/java/seedu/address/logic/commands/FindTaskCommand.java new file mode 100644 index 00000000000..67187764338 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindTaskCommand.java @@ -0,0 +1,49 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.model.Model; +import seedu.address.model.task.TaskNameContainsKeywordsPredicate; + + + +/** + * Finds and lists all tasks in task storage whose name contains any of the argument keywords. + * Keyword matching is case-insensitive. + */ +public class FindTaskCommand extends Command { + + public static final String COMMAND_WORD = "findt"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all tasks whose names/tags contain any of " + + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + + "Usage: " + + COMMAND_WORD + " " + + "KEYWORD [MORE_KEYWORDS]...[TAG]...\n" + + "Example: " + COMMAND_WORD + " lecture consultation\n" + + "Note: Include \"dt/\" tag to search based on date"; + + + private final TaskNameContainsKeywordsPredicate predicate; + + public FindTaskCommand(TaskNameContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + + model.updateFilteredTaskList(this.predicate); + return new CommandResult( + String.format(Messages.MESSAGE_TASKS_LISTED_OVERVIEW, model.getFilteredTaskList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindTaskCommand // instanceof handles nulls + && predicate.equals(((FindTaskCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/GenerateEmailsCommand.java b/src/main/java/seedu/address/logic/commands/GenerateEmailsCommand.java new file mode 100644 index 00000000000..372af7982bd --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/GenerateEmailsCommand.java @@ -0,0 +1,73 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Objects; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.task.Task; + +public class GenerateEmailsCommand extends Command { + + public static final String COMMAND_WORD = "gen"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Generates all the emails of the people related to the task " + + "identified by the index number in the task list.\n" + + "Usage: " + + COMMAND_WORD + " " + + "INDEX\n" + + "Example: " + COMMAND_WORD + " 1"; + + static final String MESSAGE_NO_CONTACTS_ASSIGNED = + "Failed: There are no contacts assigned to the task %1$s"; + + static final String MESSAGE_GENERATED_EMAILS = "Here are the emails related to the task %1$s:\n" + + "%2$s"; + + private final Index targetIndex; + + /** + * Constructs GenerateEmailsCommand that takes in a {@code targetIndex}. + * + */ + public GenerateEmailsCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List shownTaskList = model.getFilteredTaskList(); + + if (targetIndex.getZeroBased() >= shownTaskList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + Task task = shownTaskList.get(targetIndex.getZeroBased()); + + if (task.getNoOfPeople() == 0) { + return new CommandResult(String.format(MESSAGE_NO_CONTACTS_ASSIGNED, task)); + } + + String emails = task.getEmails(); + + return new CommandResult(String.format(MESSAGE_GENERATED_EMAILS, task, emails), emails); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GenerateEmailsCommand)) { + return false; + } + GenerateEmailsCommand other = (GenerateEmailsCommand) o; + return Objects.equals(this.targetIndex, other.targetIndex); + } +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..65a17a2af89 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -3,7 +3,7 @@ import seedu.address.model.Model; /** - * Format full help instructions for every command for display. + * Formats full help instructions for every command for display. */ public class HelpCommand extends Command { diff --git a/src/main/java/seedu/address/logic/commands/ImportCommand.java b/src/main/java/seedu/address/logic/commands/ImportCommand.java new file mode 100644 index 00000000000..5859ff31119 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ImportCommand.java @@ -0,0 +1,131 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILEPATH; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.util.TagUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Imports a list of contacts from a .csv file. + */ +public class ImportCommand extends Command { + + public static final String COMMAND_WORD = "import"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Imports contacts into " + + "NUS Classes from a .csv data file. " + + "Parameters: " + + PREFIX_FILEPATH + "FILEPATH\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_FILEPATH + "data.csv"; + public static final String MESSAGE_SUCCESS = + "Import completed. The following %d contact(s) were successfully added to NUS Classes from %s:\n"; + public static final String MESSAGE_DUPLICATES_NOT_ADDED = + "\nNote: The following %d contact(s) containing duplicate fields were not added:\n"; + public static final String MESSAGE_FOUND_TAGS_TOO_LONG = + "\nNote: The following %d contact(s) containing tags longer " + + "than the maximum length of 50 characters were not added:\n"; + public static final String MESSAGE_NO_CONTACTS_ADDED = + "Import completed. No contacts were added to NUS Classes from %s.\n"; + public static final String MESSAGE_INVALID_FIELDS = + "\nNote: %d contact(s) containing invalid fields were not added due to these issues:\n"; + + private final String filename; + private final List contactsToAdd; + private final List invalidFields; + + /** + * Creates an ImportCommand with the following fields + * @param contactsToAdd The contacts to be added to the model (before duplicate checks) + * @param filename The name of the data file + * @param invalidFields A list of strings, each giving info on an invalid entry that was provided in the data file. + */ + public ImportCommand(List contactsToAdd, String filename, List invalidFields) { + this.contactsToAdd = contactsToAdd; + this.filename = filename; + this.invalidFields = invalidFields; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + List personAddedContacts = new ArrayList<>(); + int addedCount = 0; + List duplicateContacts = new ArrayList<>(); + int duplicateCount = 0; + List invalidTagContacts = new ArrayList<>(); + int invalidTagCount = 0; + + for (Person p : contactsToAdd) { + boolean hasEmail = model.hasEmail(p.getEmail()); + boolean hasPhone = model.hasPhone(p.getPhone()); + boolean hasUsername = model.hasUsername(p.getUsername()); + + if (hasEmail || hasPhone || hasUsername) { + duplicateCount++; + duplicateContacts.add(p); + continue; + } + + //null value represents no tags are too long. + if (TagUtil.checkTagLength(p.getTags()) != null) { + invalidTagCount++; + invalidTagContacts.add(p); + continue; + } + addedCount++; + personAddedContacts.add(p); + model.addPerson(p); + } + + String infoAdded = addedCount == 0 + ? String.format(MESSAGE_NO_CONTACTS_ADDED, filename) + : String.format(MESSAGE_SUCCESS, addedCount, filename) + personListToString(personAddedContacts); + + String infoDuplicates = duplicateCount == 0 + ? "" + : String.format(MESSAGE_DUPLICATES_NOT_ADDED, duplicateCount) + personListToString(duplicateContacts); + + String infoInvalidTags = invalidTagCount == 0 + ? "" + : String.format(MESSAGE_FOUND_TAGS_TOO_LONG, invalidTagCount) + personListToString(invalidTagContacts); + + int invalidCount = invalidFields.size(); + + String infoInvalidFields = invalidCount == 0 + ? "" + : String.format(MESSAGE_INVALID_FIELDS, invalidCount) + String.join("\n", invalidFields); + + return new CommandResult(infoAdded + infoDuplicates + infoInvalidTags + infoInvalidFields); + } + + /** + * Converts a list of people to a string separated by a \n character. + * + * @param people The people to be converted. + * @return The result string. + */ + public static String personListToString(List people) { + return String.join("\n", () -> people.stream().map(Person::toString).iterator()) + "\n"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ImportCommand)) { + return false; + } + ImportCommand other = (ImportCommand) o; + return contactsToAdd.equals(other.contactsToAdd) + && filename.equals(other.filename) + && invalidFields.equals(other.invalidFields); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..8890f670631 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -10,7 +10,7 @@ */ public class ListCommand extends Command { - public static final String COMMAND_WORD = "list"; + public static final String COMMAND_WORD = "listc"; public static final String MESSAGE_SUCCESS = "Listed all persons"; diff --git a/src/main/java/seedu/address/logic/commands/ListTaskCommand.java b/src/main/java/seedu/address/logic/commands/ListTaskCommand.java new file mode 100644 index 00000000000..7a0b7363bf8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ListTaskCommand.java @@ -0,0 +1,65 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LIST_ALL_TASK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LIST_COMPLETE_TASK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LIST_INCOMPLETE_TASK; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_MARK_TASKS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_TASKS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_UNMARK_TASKS; + +import seedu.address.model.Model; + +/** + * Lists all tasks in the task list to the user. + */ +public class ListTaskCommand extends Command { + + public static final String COMMAND_WORD = "listt"; + + public static final String MESSAGE_SUCCESS_ALL = "Listed all tasks"; + + public static final String MESSAGE_SUCCESS_COMPLETED = "Listed all completed task(s)"; + + public static final String MESSAGE_SUCCESS_NOT_COMPLETED = "Listed all incomplete task(s)"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": List tasks\n" + + "Parameters: [" + PREFIX_LIST_ALL_TASK + "] for all (complete + incomplete) tasks or\n " + + "[" + PREFIX_LIST_INCOMPLETE_TASK + "] for incomplete tasks or\n " + + "[" + PREFIX_LIST_COMPLETE_TASK + "] for complete tasks\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_LIST_ALL_TASK; + + private String prefix; + + /** + * Constructs ListTaskCommand that takes in prefix. + * + * @param prefix prefix based on user input. + */ + public ListTaskCommand(String prefix) { + requireNonNull(prefix); + this.prefix = prefix; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + if (prefix.equals("all")) { + model.updateFilteredTaskList(PREDICATE_SHOW_ALL_TASKS); + return new CommandResult(MESSAGE_SUCCESS_ALL); + } else if (prefix.equals("c")) { + model.updateFilteredTaskList(PREDICATE_SHOW_ALL_MARK_TASKS); + return new CommandResult(MESSAGE_SUCCESS_COMPLETED); + } else { + model.updateFilteredTaskList(PREDICATE_SHOW_ALL_UNMARK_TASKS); + return new CommandResult(MESSAGE_SUCCESS_NOT_COMPLETED); + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ListTaskCommand // instanceof handles nulls + && prefix.equals(((ListTaskCommand) other).prefix)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/MarkTaskCommand.java b/src/main/java/seedu/address/logic/commands/MarkTaskCommand.java new file mode 100644 index 00000000000..9237a68c0e6 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/MarkTaskCommand.java @@ -0,0 +1,62 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.task.Task; + +/** + * Marks a task identified using it's displayed index as completed. + */ +public class MarkTaskCommand extends Command { + public static final String COMMAND_WORD = "mark"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Marks the task identified by the index number used in the displayed task list.\n" + + "Index must be a positive integer." + + "Usage: " + + COMMAND_WORD + " " + + "INDEX\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_MARK_TASK_SUCCESS = "Marked Task: %1$s"; + + private final Index targetIndex; + + /** + * Constructs MarkTaskCommand that takes in a task index. + * + * @param targetIndex the index of the task. + */ + public MarkTaskCommand(Index targetIndex) { + requireNonNull(targetIndex); + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List latestShownList = model.getFilteredTaskList(); + + if (targetIndex.getZeroBased() >= latestShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + Task taskToMark = latestShownList.get(targetIndex.getZeroBased()); + model.markTask(taskToMark); + return new CommandResult(String.format(MESSAGE_MARK_TASK_SUCCESS, taskToMark)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof MarkTaskCommand // instanceof handles nulls + && targetIndex.equals(((MarkTaskCommand) other).targetIndex)); // state check + } + +} diff --git a/src/main/java/seedu/address/logic/commands/UnassignCommand.java b/src/main/java/seedu/address/logic/commands/UnassignCommand.java new file mode 100644 index 00000000000..e41bdb5e8a5 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UnassignCommand.java @@ -0,0 +1,125 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.task.Task; + +/** + * Unassigns a contact in the contact list from a task. + */ +public class UnassignCommand extends Command { + public static final String COMMAND_WORD = "unassign"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Remove the person identified by the index number" + + "used in the displayed person list " + + "to the task identified by the index number used in the displayed task list.\n" + + "Usage: " + + COMMAND_WORD + " " + + "TASK_INDEX + " + PREFIX_PERSON + "PERSON_INDEX\n" + + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PERSON + "2"; + + public static final String MESSAGE_PREFIX = "Removed %1$s, Number: %2$s from the task `%3$s`\n"; + + public static final String NO_PERSON_ASSIGN = MESSAGE_PREFIX + + "There are currently no people assigned to this task."; + + public static final String MESSAGE_REMOVE_PERSON_FROM_TASK_SUCCESS_MULTIPLE = + MESSAGE_PREFIX + "There are currently %4$s people assigned to this task."; + + public static final String MESSAGE_REMOVE_PERSON_FROM_TASK_SUCCESS_SINGLE = + MESSAGE_PREFIX + "There is currently %4$s person assigned to this task."; + + public static final String MESSAGE_PERSON_NOT_IN_TASK = + "Failed: The person selected is not associated with the task"; + + private final Index taskIndex; + private final Index personIndex; + + /** + * Constructs UnassignCommand that takes in a task index and person index. + * + * @param taskIndex of the task in the filtered task list to be added to + * @param personIndex of the person in the filtered person list to add + */ + public UnassignCommand(Index taskIndex, Index personIndex) { + requireNonNull(taskIndex); + requireNonNull(personIndex); + + this.personIndex = personIndex; + this.taskIndex = taskIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List filteredPersonList = model.getFilteredPersonList(); + List filteredTaskList = model.getFilteredTaskList(); + + if (personIndex.getZeroBased() >= filteredPersonList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + if (taskIndex.getZeroBased() >= filteredTaskList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + Task taskToEdit = filteredTaskList.get(taskIndex.getZeroBased()); + Person personToRemove = filteredPersonList.get(personIndex.getZeroBased()); + + Task updatedTask = getUpdatedTask(personToRemove, taskToEdit); + + model.setTask(taskToEdit, updatedTask); + + int numberOfPeople = updatedTask.getNoOfPeople(); + + if (numberOfPeople == 0) { + return new CommandResult( + String.format(NO_PERSON_ASSIGN, + personToRemove.getName(), personToRemove.getPhone(), updatedTask)); + } + + if (numberOfPeople == 1) { + return new CommandResult( + String.format(MESSAGE_REMOVE_PERSON_FROM_TASK_SUCCESS_SINGLE, + personToRemove.getName(), personToRemove.getPhone(), updatedTask, numberOfPeople)); + } + + return new CommandResult( + String.format(MESSAGE_REMOVE_PERSON_FROM_TASK_SUCCESS_MULTIPLE, + personToRemove.getName(), personToRemove.getPhone(), updatedTask, numberOfPeople)); + } + + /** + * Obtains the updated task. + * + * @param personToRemove Person object to be removed. + * @param taskToUpdate Task to be changed. + * @return New edited Task. + * @throws CommandException If command format is wrong. + */ + private Task getUpdatedTask(Person personToRemove, Task taskToUpdate) throws CommandException { + List updatedList = new ArrayList<>(taskToUpdate.getPeople()); + if (!updatedList.remove(personToRemove)) { + throw new CommandException(MESSAGE_PERSON_NOT_IN_TASK); + } + Task editedTask = new Task(taskToUpdate.getName(), taskToUpdate.getDateTime(), taskToUpdate.getEndDateTime(), + updatedList, taskToUpdate.getTags(), taskToUpdate.getLink(), taskToUpdate.isTaskMark()); + return editedTask; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof UnassignCommand + && personIndex.equals(((UnassignCommand) other).personIndex) + && taskIndex.equals(((UnassignCommand) other).taskIndex)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/UnmarkTaskCommand.java b/src/main/java/seedu/address/logic/commands/UnmarkTaskCommand.java new file mode 100644 index 00000000000..a910013438f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UnmarkTaskCommand.java @@ -0,0 +1,62 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.task.Task; + +/** + * Unmarks a task identified using it's displayed index as not complete. + */ +public class UnmarkTaskCommand extends Command { + public static final String COMMAND_WORD = "unmark"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Unmarks the task identified by the index number used in the displayed task list.\n" + + "Index must be a positive integer." + + "Usage: " + + COMMAND_WORD + " " + + "INDEX\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_UNMARK_TASK_SUCCESS = "Unmarked Task: %1$s"; + + private final Index targetIndex; + + + /** + * Constructs UnmarkTaskCommand that takes in a task index. + * + * @param targetIndex the index of the task. + */ + public UnmarkTaskCommand(Index targetIndex) { + requireNonNull(targetIndex); + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List latestShownList = model.getFilteredTaskList(); + + if (targetIndex.getZeroBased() >= latestShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + Task taskToMark = latestShownList.get(targetIndex.getZeroBased()); + model.unmarkTask(taskToMark); + return new CommandResult(String.format(MESSAGE_UNMARK_TASK_SUCCESS, taskToMark)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UnmarkTaskCommand // instanceof handles nulls + && targetIndex.equals(((UnmarkTaskCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/ViewCommand.java b/src/main/java/seedu/address/logic/commands/ViewCommand.java new file mode 100644 index 00000000000..de361484bfc --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewCommand.java @@ -0,0 +1,81 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.person.PersonContainInTask; +import seedu.address.model.task.Task; + +/** + * Finds and lists all persons in address book who were assigned to a given task. + */ +public class ViewCommand extends Command { + public static final String COMMAND_WORD = "view"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Display all contacts assigned to the task identified " + + "by the index number used in the displayed task list. \n" + + "Usage: " + + COMMAND_WORD + " " + + "INDEX (must be a positive integer) \n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String NO_CONTACT_ASSIGN = "Failed: There are no contacts assigned to task %d."; + public static final String DISPLAY_TASK_CONTACT_SUCCESS = "Found %1$d contact(s) assigned to this task"; + private final Index targetIndex; + + /** + * Constructs ViewTaskCommand that takes in targetIndex. + * + * @param targetIndex The index of the task to be targeted. + */ + public ViewCommand(Index targetIndex) { + requireNonNull(targetIndex); + this.targetIndex = targetIndex; + } + + /** + * {@inheritDoc} + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + List latestShownTaskList = model.getFilteredTaskList(); + + if (targetIndex.getZeroBased() >= latestShownTaskList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + Task taskToDisplay = latestShownTaskList.get(targetIndex.getZeroBased()); + + List listOfPeople = taskToDisplay.getPeople(); + + int listSize = listOfPeople.size(); + + if (listSize < 1) { + return new CommandResult(String.format(NO_CONTACT_ASSIGN, targetIndex.getOneBased())); + } + + PersonContainInTask predicate = new PersonContainInTask(listOfPeople); + + model.updateFilteredPersonList(predicate); + + listSize = model.getFilteredPersonList().size(); + + return new CommandResult( + String.format(DISPLAY_TASK_CONTACT_SUCCESS, listSize)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ViewCommand // instanceof handles nulls + && targetIndex.equals(((ViewCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 3b8bfa035e8..adfd3ec6cd7 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -1,19 +1,19 @@ package seedu.address.logic.parser; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GIT_USERNAME; 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.arePrefixesPresent; import java.util.Set; -import java.util.stream.Stream; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.GitUsername; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -27,34 +27,57 @@ public class AddCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the AddCommand * and returns an AddCommand object for execution. - * @throws ParseException if the user input does not conform the expected format + * + * @param args String object of user input to be parsed. + * @return AddCommand object + * @throws ParseException If the input does not conform to the expected format. */ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_GIT_USERNAME, PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_GIT_USERNAME) || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + StringBuffer sb = displayInvalidParameters(argMultimap); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, sb + "\n" + + AddCommand.MESSAGE_USAGE)); } Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + GitUsername gitUsername = ParserUtil.parseGitUsername(argMultimap.getValue(PREFIX_GIT_USERNAME).get()); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, email, gitUsername, tagList); return new AddCommand(person); } /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. + * Checks what parameters are missing in user's input. Returns the tags that are missing. + * Example: If n/ and p/ are missing, return "Missing/Invalid parameters: n/, p/". + * + * @param argMultimap Argument Multimap of user input that is read. + * @return StringBuffer format of missing parameters. */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + public StringBuffer displayInvalidParameters(ArgumentMultimap argMultimap) { + String errorString = "Missing/Invalid parameters: "; + if (!arePrefixesPresent(argMultimap, PREFIX_NAME)) { + errorString += PREFIX_NAME + ", "; + } + if (!arePrefixesPresent(argMultimap, PREFIX_PHONE)) { + errorString += PREFIX_PHONE + ", "; + } + if (!arePrefixesPresent(argMultimap, PREFIX_EMAIL)) { + errorString += PREFIX_EMAIL + ", "; + } + if (!arePrefixesPresent(argMultimap, PREFIX_GIT_USERNAME)) { + errorString += PREFIX_GIT_USERNAME + ", "; + } + StringBuffer sb = new StringBuffer(errorString); + sb.delete(sb.length() - 2, sb.length() - 1); //Deleting last comma + return sb; } - } 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..2dd540fd85b --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddTaskCommandParser.java @@ -0,0 +1,152 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_DATETIME; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_INTERVAL; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_RECURRENCE; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_RECURRENCE_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATETIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_RECURRING; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TASKNAME; +import static seedu.address.logic.parser.ParserUtil.arePrefixesPresent; + +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Map; +import java.util.Set; + +import seedu.address.commons.util.TranslatorUtil; +import seedu.address.logic.commands.AddTaskCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; +import seedu.address.model.task.Link; + +/** + * Parses input arguments and creates a new AddTaskCommand object + */ +public class AddTaskCommandParser implements Parser { + + private final String dateTimePattern = "dd-MM-yyyy HHmm"; + private final SimpleDateFormat dateTimeFormatter = new SimpleDateFormat(dateTimePattern); + + /** + * Parses the given {@code String} of arguments in the context of the AddTaskCommand for TASKNAME, DATETIME, TAG + * and returns an AddTaskCommand object for execution. + * + * @param args String object of user input to be parsed. + * @return AddTaskCommand object + * @throws ParseException If the input does not conform to the expected format. + */ + public AddTaskCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_TASKNAME, PREFIX_DATETIME, + PREFIX_TAG, PREFIX_LINK, PREFIX_RECURRING); + + if (!arePrefixesPresent(argMultimap, PREFIX_TASKNAME, PREFIX_DATETIME) + || !argMultimap.getPreamble().isEmpty()) { + StringBuffer sb = displayInvalidParameters(argMultimap); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, sb + "\n" + + AddTaskCommand.MESSAGE_USAGE)); + } + + String taskName = ParserUtil.parseTaskName(argMultimap.getValue(PREFIX_TASKNAME)); + String dateTimeString = argMultimap.getValue(PREFIX_DATETIME).get(); + LocalDateTime dateTime; + LocalDateTime endDateTime; + Set tags = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + Link link = ParserUtil.parseLink(argMultimap.getValue(PREFIX_LINK)); + + try { + dateTimeFormatter.setLenient(false); + + if (dateTimeString.contains(",")) { + String[] splits = dateTimeString.split(","); + dateTime = convertToLocalDateTime(dateTimeFormatter.parse(splits[0])); + endDateTime = convertToLocalDateTime(dateTimeFormatter.parse(splits[1])); + } else { + dateTime = convertToLocalDateTime(dateTimeFormatter.parse(dateTimeString)); + endDateTime = null; + } + } catch (java.text.ParseException e) { + throw new ParseException(String.format(MESSAGE_INVALID_DATETIME, AddTaskCommand.MESSAGE_USAGE)); + } + + // If recurring tag is present in argument + if (arePrefixesPresent(argMultimap, PREFIX_RECURRING)) { + int periodInt = 0; + int recurrenceInt = 0; + + String[] periodMultipleArr = ParserUtil.parseRecurring(argMultimap.getValue(PREFIX_RECURRING)); + + String periodStr = periodMultipleArr[0].toLowerCase(); + String recurrenceStr = periodMultipleArr[1]; + + Map periodMapping = TranslatorUtil.getPeriodMapping(); + + if (periodMapping.containsKey(periodStr)) { + periodInt = periodMapping.get(periodStr); + } else { + periodInt = parsePeriod(periodStr); + } + + try { + recurrenceInt = Integer.parseInt(recurrenceStr); + } catch (NumberFormatException e) { + throw new ParseException(String.format(MESSAGE_INVALID_RECURRENCE, + AddTaskCommand.MESSAGE_USAGE)); + } + + if (periodInt <= 0 || recurrenceInt <= 0) { + throw new ParseException(String.format(MESSAGE_INVALID_RECURRENCE_INDEX, + AddTaskCommand.MESSAGE_USAGE)); + } + + return new AddTaskCommand(taskName, dateTime, endDateTime, tags, link, periodInt, recurrenceInt); + } + + return new AddTaskCommand(taskName, dateTime, endDateTime, tags, link); + } + + /** + * Converts a date object to LocalDateTime object. + * + * @param dateToConvert Date object to be converted. + * @return LocalDateTime object. + */ + LocalDateTime convertToLocalDateTime(Date dateToConvert) { + return new java.sql.Timestamp( + dateToConvert.getTime()).toLocalDateTime(); + } + + /** + * Checks what parameters are missing in user's input. Returns the tags that are missing. + * Example: If tn/ and dt/ are missing, return "Missing/Invalid parameters: tn/, dt/". + * + * @param argMultimap Argument Multimap of user input that is read. + * @return StringBuffer format of missing parameters. + */ + public StringBuffer displayInvalidParameters(ArgumentMultimap argMultimap) { + String errorString = "Missing/Invalid parameters: "; + if (!arePrefixesPresent(argMultimap, PREFIX_TASKNAME)) { + errorString += PREFIX_TASKNAME + ", "; + } + if (!arePrefixesPresent(argMultimap, PREFIX_DATETIME)) { + errorString += PREFIX_DATETIME + ", "; + } + StringBuffer sb = new StringBuffer(errorString); + sb.delete(sb.length() - 2, sb.length() - 1); //Deleting last comma + return sb; + } + + private int parsePeriod(String periodStr) throws ParseException { + try { + return Integer.parseInt(periodStr); + } catch (NumberFormatException e) { + throw new ParseException(String.format(MESSAGE_INVALID_INTERVAL, + AddTaskCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 1e466792b46..f2c70b55551 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -2,19 +2,32 @@ import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATETIME; import java.util.regex.Matcher; import java.util.regex.Pattern; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddTaskCommand; +import seedu.address.logic.commands.AssignCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteTaskCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditTaskCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindTaskCommand; +import seedu.address.logic.commands.GenerateEmailsCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ImportCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ListTaskCommand; +import seedu.address.logic.commands.MarkTaskCommand; +import seedu.address.logic.commands.UnassignCommand; +import seedu.address.logic.commands.UnmarkTaskCommand; +import seedu.address.logic.commands.ViewCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -59,15 +72,55 @@ public Command parseCommand(String userInput) throws ParseException { case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case FindTaskCommand.COMMAND_WORD: + if (arguments.contains(PREFIX_DATETIME.toString())) { + return new FilterByDateTimeParser().parse(arguments); + } else { + return new FilterCommandParser().parse(arguments); + } + case ListCommand.COMMAND_WORD: return new ListCommand(); + case ListTaskCommand.COMMAND_WORD: + return new ListTaskCommandParser().parse(arguments); + case ExitCommand.COMMAND_WORD: return new ExitCommand(); case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case AddTaskCommand.COMMAND_WORD: + return new AddTaskCommandParser().parse(arguments); + + case DeleteTaskCommand.COMMAND_WORD: + return new DeleteTaskCommandParser().parse(arguments); + + case AssignCommand.COMMAND_WORD: + return new AssignCommandParser().parse(arguments); + + case UnassignCommand.COMMAND_WORD: + return new UnassignCommandParser().parse(arguments); + + case ViewCommand.COMMAND_WORD: + return new ViewCommandParser().parse(arguments); + + case EditTaskCommand.COMMAND_WORD: + return new EditTaskCommandParser().parse(arguments); + + case GenerateEmailsCommand.COMMAND_WORD: + return new GenerateEmailsCommandParser().parse(arguments); + + case MarkTaskCommand.COMMAND_WORD: + return new MarkTaskCommandParser().parse(arguments); + + case UnmarkTaskCommand.COMMAND_WORD: + return new UnmarkTaskCommandParser().parse(arguments); + + case ImportCommand.COMMAND_WORD: + return new ImportCommandParser().parse(arguments); + default: throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } diff --git a/src/main/java/seedu/address/logic/parser/AssignCommandParser.java b/src/main/java/seedu/address/logic/parser/AssignCommandParser.java new file mode 100644 index 00000000000..7375e9c0eee --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AssignCommandParser.java @@ -0,0 +1,52 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; +import static seedu.address.logic.parser.ParserUtil.arePrefixesPresent; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AssignCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AssignCommand object + */ +public class AssignCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the AssignCommand + * and returns an AssignCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AssignCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_PERSON); + + Index taskIndex; + Index personIndex; + + if (!arePrefixesPresent(argMultimap, PREFIX_PERSON)) { + String missingParameterMessage = displayInvalidParameters(argMultimap); + String errorMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, missingParameterMessage + + "\n" + AssignCommand.MESSAGE_USAGE); + throw new ParseException(errorMessage); + } + + taskIndex = ParserUtil.parseIndex(argMultimap.getPreamble()); + personIndex = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_PERSON).get()); + + return new AssignCommand(taskIndex, personIndex); + } + + /** + * Checks what parameters are missing in user's input. Returns the tags that are missing. + * Example: If p/ are missing, return "Missing/Invalid parameters: p/". + * + * @param argMultimap Argument Multimap of user input that is read. + * @return String format of missing parameters. + */ + public String displayInvalidParameters(ArgumentMultimap argMultimap) { + return "Missing/Invalid parameters: " + PREFIX_PERSON; + } +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..cc590b079cb 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -12,4 +12,15 @@ public class CliSyntax { public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + /* Added prefix definitions */ + public static final Prefix PREFIX_TASKNAME = new Prefix("tn/"); + public static final Prefix PREFIX_DATETIME = new Prefix("dt/"); + public static final Prefix PREFIX_PERSON = new Prefix("p/"); + public static final Prefix PREFIX_GIT_USERNAME = new Prefix("u/"); + public static final Prefix PREFIX_LINK = new Prefix("z/"); + public static final Prefix PREFIX_RECURRING = new Prefix("r/"); + public static final Prefix PREFIX_LIST_ALL_TASK = new Prefix("all/"); + public static final Prefix PREFIX_LIST_INCOMPLETE_TASK = new Prefix("nc/"); + public static final Prefix PREFIX_LIST_COMPLETE_TASK = new Prefix("c/"); + public static final Prefix PREFIX_FILEPATH = new Prefix("fp/"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 522b93081cc..c3879e8a378 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -1,6 +1,6 @@ package seedu.address.logic.parser; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeleteCommand; @@ -22,7 +22,7 @@ public DeleteCommand parse(String args) throws ParseException { return new DeleteCommand(index); } catch (ParseException pe) { throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + String.format(MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, DeleteCommand.MESSAGE_USAGE), pe); } } 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..0b7f44932b7 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteTaskCommandParser.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX; + +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 user input does not conform the expected format + */ + public DeleteTaskCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteTaskCommand(index); + } catch (ParseException pe) { + throw new ParseException(MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 845644b7dea..bd4d36b92a9 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -1,9 +1,9 @@ package seedu.address.logic.parser; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GIT_USERNAME; 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; @@ -32,14 +32,14 @@ public class EditCommandParser implements Parser { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_GIT_USERNAME, PREFIX_TAG); Index index; - try { index = ParserUtil.parseIndex(argMultimap.getPreamble()); } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); + throw new ParseException(MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -52,9 +52,12 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + + if (argMultimap.getValue(PREFIX_GIT_USERNAME).isPresent()) { + editPersonDescriptor.setUsername(ParserUtil + .parseGitUsername(argMultimap.getValue(PREFIX_GIT_USERNAME).get())); } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { diff --git a/src/main/java/seedu/address/logic/parser/EditTaskCommandParser.java b/src/main/java/seedu/address/logic/parser/EditTaskCommandParser.java new file mode 100644 index 00000000000..9bf967de79c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditTaskCommandParser.java @@ -0,0 +1,114 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_DATETIME; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX; +import static seedu.address.commons.core.Messages.MESSAGE_NEED_AT_LEAST_ONE_VALID_PARAMETER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATETIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LINK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TASKNAME; +import static seedu.address.logic.parser.ParserUtil.arePrefixesPresent; + +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditTaskCommand; +import seedu.address.logic.commands.EditTaskCommand.EditTaskDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new EditTaskCommand object + */ +public class EditTaskCommandParser implements Parser { + private final String dateTimePattern = "dd-MM-yyyy HHmm"; + private final SimpleDateFormat dateTimeFormatter = new SimpleDateFormat(dateTimePattern); + + /** + * Parses the given {@code String} of arguments in the context of the EditTaskCommand + * and returns an EditTaskCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditTaskCommand parse(String args) throws ParseException { + requireNonNull(args); + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_TASKNAME, PREFIX_DATETIME, PREFIX_TAG, PREFIX_LINK); + + if (!arePrefixesPresent(argMultimap, PREFIX_TASKNAME) && !arePrefixesPresent(argMultimap, PREFIX_DATETIME) + && !arePrefixesPresent(argMultimap, PREFIX_TAG) && !arePrefixesPresent(argMultimap, PREFIX_LINK)) { + String errorMessage = MESSAGE_NEED_AT_LEAST_ONE_VALID_PARAMETER; + throw new ParseException(String.format(errorMessage, EditTaskCommand.MESSAGE_USAGE)); + } + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + + EditTaskDescriptor editTaskDescriptor = new EditTaskDescriptor(); + if (argMultimap.getValue(PREFIX_TASKNAME).isPresent()) { + String taskName = ParserUtil.parseTaskName(argMultimap.getValue(PREFIX_TASKNAME)); + editTaskDescriptor.setName(taskName); + } + if (argMultimap.getValue(PREFIX_DATETIME).isPresent()) { + try { + dateTimeFormatter.setLenient(false); + String dateTimeString = argMultimap.getValue(PREFIX_DATETIME).get(); + if (dateTimeString.contains(",")) { + String[] splits = dateTimeString.split(","); + editTaskDescriptor.setDate(convertToLocalDateTime(dateTimeFormatter.parse(splits[0]))); + editTaskDescriptor.setEndDate(convertToLocalDateTime(dateTimeFormatter.parse(splits[1]))); + } else { + editTaskDescriptor.setDate(convertToLocalDateTime(dateTimeFormatter.parse(dateTimeString))); + editTaskDescriptor.setEndDate(null); + } + } catch (java.text.ParseException e) { + throw new ParseException(String.format(MESSAGE_INVALID_DATETIME, EditTaskCommand.MESSAGE_USAGE)); + } + } + + if (argMultimap.getValue(PREFIX_LINK).isPresent()) { + editTaskDescriptor.setLink(ParserUtil.parseLink(argMultimap.getValue(PREFIX_LINK))); + } + + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editTaskDescriptor::setTags); + + if (!editTaskDescriptor.isAnyFieldEdited()) { + throw new ParseException(String.format(EditTaskCommand.MESSAGE_NOT_EDITED, EditTaskCommand.MESSAGE_USAGE)); + } + + return new EditTaskCommand(index, editTaskDescriptor); + } + + private LocalDateTime convertToLocalDateTime(Date dateToConvert) { + return new java.sql.Timestamp( + dateToConvert.getTime()).toLocalDateTime(); + } + + /** + * 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/FilterByDateTimeParser.java b/src/main/java/seedu/address/logic/parser/FilterByDateTimeParser.java new file mode 100644 index 00000000000..caaa27cc95d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FilterByDateTimeParser.java @@ -0,0 +1,180 @@ +package seedu.address.logic.parser; + + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.ERROR_MESSAGE_INVALID_FORMAT; +import static seedu.address.commons.core.Messages.ERROR_MESSAGE_INVALID_PARAMETER; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import seedu.address.logic.commands.FilterByDateCommand; +import seedu.address.logic.commands.FindTaskCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.task.TaskBetweenDatesPredicate; +/** + * Parses input arguments and creates a new FilterByDate Command + */ +public class FilterByDateTimeParser implements Parser { + + private static final String DATE_TIME_PATTERN = "dd-MM-yyyy HHmm"; + private static final String DATE_ONLY_PATTERN = "dd-MM-yyyy"; + private SimpleDateFormat dateTimeFormatter = new SimpleDateFormat(DATE_TIME_PATTERN); + private SimpleDateFormat dateOnlyFormatter = new SimpleDateFormat(DATE_ONLY_PATTERN); + + /** + * Parses the given {@code String} of arguments in the context of the FilterByDateTimeCommand + * and returns a FilterByDateCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public FilterByDateCommand parse(String args) throws ParseException { + requireNonNull(args); + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindTaskCommand.MESSAGE_USAGE)); + } + + return new FilterByDateCommand(new TaskBetweenDatesPredicate(inBetweenDates(trimmedArgs))); + } + + /** + * Gets the before and after date from a String of dates, e.g. "filter dt/22-08-2022 0800,23-08-2023 0800" + * + * @param dates dates to be seperated + * @return A list of before and after dates + */ + public List inBetweenDates(String dates) throws DateTimeParseException, ParseException { + // from "dt/22-08-2022 0800,23-08-2022 0800" to ["dt", "22-08-2022 0800", "23-08-2022 0800"] + String[] splitDates = dates.split("[/,]"); + if (splitDates.length != 3) { + throw new ParseException(ERROR_MESSAGE_INVALID_PARAMETER); + } + + // check if time is provided + if (checkTime(splitDates[1]) && checkTime(splitDates[2])) { + return localDateTimeChecker(dayMonthYearTime(splitDates[1]), dayMonthYearTime(splitDates[2])); + } else if (!checkTime(splitDates[1]) && !checkTime(splitDates[2])) { + return timeAdder(localDateTimeChecker(dayMonthYear(splitDates[1]), dayMonthYear(splitDates[2]))); + } else if (checkTime(splitDates[1]) && !checkTime(splitDates[2])) { + return localDateTimeTargetedAdder(dayMonthYearTime(splitDates[1]), + dayMonthYear(splitDates[2]), true); + } else if (!checkTime(splitDates[1]) && checkTime(splitDates[2])) { + return localDateTimeTargetedAdder(dayMonthYear(splitDates[1]), + dayMonthYearTime(splitDates[2]), false); + } else { + throw new ParseException(ERROR_MESSAGE_INVALID_FORMAT); + } + } + + /** + * Check if input contains time + * + * @param datetime String datetime input form user + * @return true if date time contains time, else return false + */ + private boolean checkTime(String datetime) { + return datetime.trim().split("[- ]").length == 4; + } + + /** + * Converts user date only input into a list containing 2 date time elements + * + * @param datetime String of date without time in the format dd-MM-yyyy + * @return List of date sorted, first date at 0000 hrs (lower bound) and second date at 2359 hrs (upper bound) + * @throws ParseException Invalid date format + */ + private LocalDateTime dayMonthYear(String datetime) throws ParseException { + try { + dateOnlyFormatter.setLenient(false); + return convertToLocalDateTime(dateOnlyFormatter.parse(datetime)); + } catch (java.text.ParseException e) { + throw new ParseException(ERROR_MESSAGE_INVALID_FORMAT); + } + } + /** + * Converts user date time input into a list containing 2 date time elements + * + * @param datetime String of date time in the format dd-MM-yyyy HHmm + * @return List of date sorted, first dt being lower bound, second dt being upper bound + * @throws ParseException Invalid date format + */ + private LocalDateTime dayMonthYearTime(String datetime) throws ParseException { + try { + dateTimeFormatter.setLenient(false); + return convertToLocalDateTime(dateTimeFormatter.parse(datetime)); + } catch (java.text.ParseException e) { + throw new ParseException(ERROR_MESSAGE_INVALID_FORMAT); + } + } + + /** + * Sort 2 given date time + * + * @param firstDateTime first date time to be sorted + * @param secondDateTime second date time to be sorted + * @return A list of date time, first dt being the earlier one, second dt being the later one + */ + private List localDateTimeChecker(LocalDateTime firstDateTime, LocalDateTime secondDateTime) { + if (firstDateTime.isBefore(secondDateTime)) { + return Arrays.asList(firstDateTime, secondDateTime); + } else { + return Arrays.asList(secondDateTime, firstDateTime); + } + } + + /** + * Sort 2 given date time, where only 1 of them have time + * + * @param firstDateTime first date time to be sorted + * @param secondDateTime second date time to be sorted + * @param firstDateContainsTime is true if first date contains time + * @return A list of date time, first dt being the earlier one, second dt being the later one + * @throws ParseException Invalid date time format + */ + private List localDateTimeTargetedAdder(LocalDateTime firstDateTime, + LocalDateTime secondDateTime, + boolean firstDateContainsTime) throws ParseException { + if (firstDateTime.isBefore(secondDateTime) && firstDateContainsTime) { + return Arrays.asList(firstDateTime, secondDateTime.plusHours(23).plusMinutes(59)); + } else if (firstDateTime.isBefore(secondDateTime) && !firstDateContainsTime) { + return Arrays.asList(firstDateTime, secondDateTime); + } else if (!firstDateTime.isBefore(secondDateTime) && firstDateContainsTime) { + return Arrays.asList(secondDateTime, firstDateTime); + } else if (!firstDateTime.isBefore(secondDateTime) && !firstDateContainsTime) { + return Arrays.asList(secondDateTime, firstDateTime.plusHours(23).plusMinutes(59)); + } else { + throw new ParseException(MESSAGE_INVALID_COMMAND_FORMAT); + } + } + + /** + * Adds upperbound time 23 hours and 59 min, in order to give it the property of full day search + * e.g. dt/21-02-2022, 22-02-2022 -> 21 Feb 2022 12mn, 22 Feb 2022 11:59pm + * + * @param listOfDates Sorted date time list + * @return Sorted date time list with proper time + */ + private List timeAdder(List listOfDates) { + LocalDateTime setUpperBoundTiming = listOfDates.get(1).plusHours(23).plusMinutes(59); + listOfDates.set(1, setUpperBoundTiming); + return listOfDates; + } + + /** + * Inspired from .\src\main\java\seedu\address\logic\parser\EditTaskCommandParser.java + * + * @param dateToConvert date to be converted + * @return Local Date Time + */ + private LocalDateTime convertToLocalDateTime(Date dateToConvert) { + return new java.sql.Timestamp( + dateToConvert.getTime()).toLocalDateTime(); + } +} 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..6efd97027cc --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java @@ -0,0 +1,35 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_NO_KEYWORDS; + +import java.util.Arrays; + +import seedu.address.logic.commands.FindTaskCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.task.TaskNameContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FindCommand object + */ +public class FilterCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns a FindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindTaskCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_NO_KEYWORDS, FindTaskCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + + return new FindTaskCommand(new TaskNameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + } + + + +} diff --git a/src/main/java/seedu/address/logic/parser/GenerateEmailsCommandParser.java b/src/main/java/seedu/address/logic/parser/GenerateEmailsCommandParser.java new file mode 100644 index 00000000000..25b9bb0c91a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/GenerateEmailsCommandParser.java @@ -0,0 +1,23 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.GenerateEmailsCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new GenerateEmailsCommand object + */ +public class GenerateEmailsCommandParser implements Parser { + + @Override + public GenerateEmailsCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new GenerateEmailsCommand(index); + } catch (ParseException pe) { + throw new ParseException(MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/ImportCommandParser.java b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java new file mode 100644 index 00000000000..9f9b8baee7f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java @@ -0,0 +1,142 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILEPATH; +import static seedu.address.logic.parser.ParserUtil.arePrefixesPresent; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Scanner; +import java.util.Set; + +import seedu.address.logic.commands.ImportCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Email; +import seedu.address.model.person.GitUsername; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new ImportCommand object + */ +public class ImportCommandParser implements Parser { + + public static final String MESSAGE_CSV_MISSING_HEADERS = "Missing headers in the file \"%s\""; + + public static final String MESSAGE_FOLDER_SPECIFIED = "Error: \"%s\" is a directory"; + + public static final String MESSAGE_FILE_DOES_NOT_EXIST = "Error: could not find the file \"%s\"."; + + public static final String ERROR_INVALID_NAME = "Error: the name \"%s\" is invalid: " + Name.MESSAGE_CONSTRAINTS; + + public static final String ERROR_INVALID_PHONE = "Error: the phone \"%s\" is invalid: " + Phone.MESSAGE_CONSTRAINTS; + + public static final String ERROR_INVALID_EMAIL = "Error: the email \"%s\" is invalid: " + + Email.MESSAGE_CONSTRAINTS; + + public static final String ERROR_INVALID_GITHUB = "Error: the github username\"%s\" is invalid: " + + GitUsername.MESSAGE_CONSTRAINTS; + + public static final String ERROR_INVALID_TAG = "Error: the tag\"%s\" is invalid: " + + Tag.MESSAGE_CONSTRAINTS; + + + + /** + * Parses the given {@code String} of arguments in the context of the ImportCommand + * and returns a ImportCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public ImportCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_FILEPATH); + + if (!arePrefixesPresent(argMultimap, PREFIX_FILEPATH)) { + throw new ParseException(ImportCommand.MESSAGE_USAGE); + } + + File file = ParserUtil.parsePath(argMultimap.getValue(PREFIX_FILEPATH)).toFile(); + + if (!file.exists()) { + throw new ParseException(String.format(MESSAGE_FILE_DOES_NOT_EXIST, file.getPath())); + } + + if (file.isDirectory()) { + throw new ParseException(String.format(MESSAGE_FOLDER_SPECIFIED, file.getPath())); + } + + List personToAddList = new ArrayList<>(); + + List invalidFields = new ArrayList<>(); + + try { + Scanner sc = new Scanner(file); + List columns = Arrays.asList(sc.nextLine().split(",")); + int nameIndex = columns.indexOf("Name"); + int phoneIndex = columns.indexOf("Phone"); + int emailIndex = columns.indexOf("Email"); + int githubIndex = columns.indexOf("Github"); + int tagsIndex = columns.indexOf("Tags"); + + if (List.of(nameIndex, phoneIndex, emailIndex, githubIndex, tagsIndex).contains(-1)) { + throw new ParseException(String.format(MESSAGE_CSV_MISSING_HEADERS, file.getPath())); + } + + loop: while (sc.hasNextLine()) { + String[] values = sc.nextLine().split(","); + Name name; + Phone phone; + Email email; + GitUsername gitUsername; + Set tags = new HashSet<>(); + + try { + name = ParserUtil.parseName(values[nameIndex]); + } catch (ParseException e) { + invalidFields.add(String.format(ERROR_INVALID_NAME, values[nameIndex])); + continue; + } + try { + phone = ParserUtil.parsePhone(values[phoneIndex]); + } catch (ParseException e) { + invalidFields.add(String.format(ERROR_INVALID_PHONE, values[phoneIndex])); + continue; + } + try { + email = ParserUtil.parseEmail(values[emailIndex]); + } catch (ParseException e) { + invalidFields.add(String.format(ERROR_INVALID_EMAIL, values[emailIndex])); + continue; + } + + try { + gitUsername = ParserUtil.parseGitUsername(values[githubIndex]); + } catch (ParseException e) { + invalidFields.add(String.format(ERROR_INVALID_GITHUB, values[githubIndex])); + continue; + } + + for (String tag : values[tagsIndex].split("/")) { + try { + tags.add(ParserUtil.parseTag(tag)); + } catch (ParseException e) { + invalidFields.add(String.format(ERROR_INVALID_TAG, tag)); + continue loop; + } + } + + personToAddList.add(new Person(name, phone, email, gitUsername, tags)); + } + + } catch (FileNotFoundException e) { + throw new ParseException(String.format(MESSAGE_CSV_MISSING_HEADERS, file.getPath())); + } + + return new ImportCommand(personToAddList, argMultimap.getValue(PREFIX_FILEPATH).get(), invalidFields); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ListTaskCommandParser.java b/src/main/java/seedu/address/logic/parser/ListTaskCommandParser.java new file mode 100644 index 00000000000..fb36fba6e8e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ListTaskCommandParser.java @@ -0,0 +1,39 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LIST_ALL_TASK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LIST_COMPLETE_TASK; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LIST_INCOMPLETE_TASK; + +import seedu.address.logic.commands.ListTaskCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Lists the task from the task list based on prefix. + */ +public class ListTaskCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the ListTaskCommand + * and returns an ListTaskCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ListTaskCommand parse(String args) throws ParseException { + requireNonNull(args); + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_LIST_ALL_TASK, + PREFIX_LIST_INCOMPLETE_TASK, PREFIX_LIST_COMPLETE_TASK); + + if (argMultimap.getValue(PREFIX_LIST_ALL_TASK).isPresent()) { + return new ListTaskCommand("all"); + } else if (argMultimap.getValue(PREFIX_LIST_INCOMPLETE_TASK).isPresent()) { + return new ListTaskCommand("nc"); + } else if (argMultimap.getValue(PREFIX_LIST_COMPLETE_TASK).isPresent()) { + return new ListTaskCommand("c"); + } else { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ListTaskCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/MarkTaskCommandParser.java b/src/main/java/seedu/address/logic/parser/MarkTaskCommandParser.java new file mode 100644 index 00000000000..396720066a0 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/MarkTaskCommandParser.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.MarkTaskCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new MarkTaskCommand object + */ +public class MarkTaskCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the MarkTaskCommand + * and returns a MarkTaskCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public MarkTaskCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new MarkTaskCommand(index); + } catch (ParseException pe) { + throw new ParseException(MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..fa931d2fff4 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -1,26 +1,34 @@ package seedu.address.logic.parser; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import java.nio.file.Path; import java.util.Collection; import java.util.HashSet; +import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.commands.AddTaskCommand; +import seedu.address.logic.commands.ImportCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.GitUsername; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; +import seedu.address.model.task.Link; +import seedu.address.model.task.Task; /** * Contains utility methods used for parsing strings in the various *Parser classes. */ public class ParserUtil { - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer!"; /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be @@ -65,20 +73,6 @@ public static Phone parsePhone(String phone) throws ParseException { return new Phone(trimmedPhone); } - /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code address} is invalid. - */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); - } - return new Address(trimmedAddress); - } /** * Parses a {@code String email} into an {@code Email}. @@ -92,9 +86,48 @@ public static Email parseEmail(String email) throws ParseException { if (!Email.isValidEmail(trimmedEmail)) { throw new ParseException(Email.MESSAGE_CONSTRAINTS); } + + if (!Email.isValidLength(trimmedEmail)) { + throw new ParseException(Email.MESSAGE_CONSTRAINTS); + } + return new Email(trimmedEmail); } + /** + * Parses task name + * + * @param option String input for Git username + * @return The Task name. + */ + public static String parseTaskName(Optional option) throws ParseException { + requireNonNull(option); + + String trimmedUsername = option.get().trim(); + if (!Task.isValidLength(trimmedUsername)) { + throw new ParseException(Task.NAME_LENGTH_ERROR); + } + + return trimmedUsername; + } + + /** + * Parses Git username. Only allows AlphaNumeric and hyphens, as per GitHub's username formats. + * Spaces are not allowed. + * + * @param gitUsername String input for Git username + * @return GitUsername object created using user input + * @throws ParseException If gitUsername is not in alphanumeric format or has symbols other than hyphens. + */ + public static GitUsername parseGitUsername(String gitUsername) throws ParseException { + requireNonNull(gitUsername); + String trimmedUsername = gitUsername.trim(); + if (!GitUsername.isValidId(trimmedUsername)) { + throw new ParseException(GitUsername.MESSAGE_CONSTRAINTS); + } + return new GitUsername(trimmedUsername); + } + /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. @@ -121,4 +154,62 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses {@Code Optional option} into a {@code Link}. + */ + public static Link parseLink(Optional option) throws ParseException { + requireNonNull(option); + if (option.isEmpty()) { + return new Link(); + } else { + if (!Link.isValidLink(option.get())) { + throw new ParseException(Link.MESSAGE_CONSTRAINTS); + } + return new Link(option.get()); + } + } + + /** + * Parses {@Code Optional option} into a {@code String[]}. + */ + public static String[] parseRecurring(Optional option) throws ParseException { + requireNonNull(option); + if (option.isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTaskCommand.MESSAGE_USAGE)); + } + + String arg = option.get(); + String[] commands = arg.split(" "); + + if (commands.length != 2) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTaskCommand.MESSAGE_USAGE)); + } + + return commands; + } + + /** + * Checks if the given prefixes are provided in the argMultimap + */ + public static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + /** + * Creates and returns the Path of the import file. + * + * @param option Optional containing the filepath + * @return The path of the file + * @throws ParseException if no filepath is provide + */ + public static Path parsePath(Optional option) throws ParseException { + requireNonNull(option); + + if (option.isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE)); + } + + return Path.of(option.get()); + } } diff --git a/src/main/java/seedu/address/logic/parser/UnassignCommandParser.java b/src/main/java/seedu/address/logic/parser/UnassignCommandParser.java new file mode 100644 index 00000000000..400b59407bb --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/UnassignCommandParser.java @@ -0,0 +1,53 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERSON; +import static seedu.address.logic.parser.ParserUtil.arePrefixesPresent; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.UnassignCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new unassignCommand object + */ +public class UnassignCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the UnassignCommand + * and returns an UnassignCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public UnassignCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_PERSON); + + Index taskIndex; + Index personIndex; + + if (!arePrefixesPresent(argMultimap, PREFIX_PERSON)) { + String missingParameterMessage = displayInvalidParameters(argMultimap); + String errorMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, missingParameterMessage + + "\n" + UnassignCommand.MESSAGE_USAGE); + throw new ParseException(errorMessage); + } + + taskIndex = ParserUtil.parseIndex(argMultimap.getPreamble()); + personIndex = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_PERSON).get()); + + return new UnassignCommand(taskIndex, personIndex); + } + + /** + * Checks what parameters are missing in user's input. Returns the tags that are missing. + * Example: If p/ are missing, return "Missing/Invalid parameters: p/". + * + * @param argMultimap Argument Multimap of user input that is read. + * @return String format of missing parameters. + */ + public String displayInvalidParameters(ArgumentMultimap argMultimap) { + return "Missing/Invalid parameters: " + PREFIX_PERSON; + } + +} diff --git a/src/main/java/seedu/address/logic/parser/UnmarkTaskCommandParser.java b/src/main/java/seedu/address/logic/parser/UnmarkTaskCommandParser.java new file mode 100644 index 00000000000..751bdef4388 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/UnmarkTaskCommandParser.java @@ -0,0 +1,27 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.UnmarkTaskCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new UnmarkTaskCommand object + */ +public class UnmarkTaskCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the UnmarkTaskCommand + * and returns a UnmarkTaskCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public UnmarkTaskCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new UnmarkTaskCommand(index); + } catch (ParseException pe) { + throw new ParseException(MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/ViewCommandParser.java b/src/main/java/seedu/address/logic/parser/ViewCommandParser.java new file mode 100644 index 00000000000..710c34dafc6 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ViewCommandParser.java @@ -0,0 +1,27 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ViewCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ViewTaskCommand object + */ +public class ViewCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ViewTaskCommand + * and returns a ViewTaskCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ViewCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new ViewCommand(index); + } catch (ParseException pe) { + throw new ParseException(MESSAGE_INVALID_TASK_DISPLAYED_INDEX); + } + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 1a943a0781a..45b2c26cf91 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -5,7 +5,10 @@ import java.util.List; import javafx.collections.ObservableList; +import seedu.address.model.person.Email; +import seedu.address.model.person.GitUsername; import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; import seedu.address.model.person.UniquePersonList; /** @@ -59,13 +62,57 @@ public void resetData(ReadOnlyAddressBook newData) { //// person-level operations /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a person with the same attributes as {@code person} exists in the address book. */ public boolean hasPerson(Person person) { requireNonNull(person); return persons.contains(person); } + /** + * Returns true if gitUsername already exists in the address book. + * + * @param gitUsername Github gitUsername to be checked + * @return true/false whether gitUsername already exists. + */ + public boolean hasUsername(GitUsername gitUsername) { + requireNonNull(gitUsername); + return persons.containsUsername(gitUsername); + } + + /** + * Checks if AddressBook has an existing contact with this email. + * + * @param email Email to be checked. + * @return Whether email exists. + */ + public boolean hasEmail(Email email) { + requireNonNull(email); + return persons.containsEmail(email); + } + + /** + * Checks if AddressBook has an existing contact with this phone number. + * + * @param phone Phone number to be checked. + * @return Whether phone number exists. + */ + public boolean hasPhone(Phone phone) { + requireNonNull(phone); + return persons.containsPhone(phone); + } + + /** + * Checks if AddressBook has an existing contact with this GitUsername. + * + * @param gitUsername Git username to be checked. + * @return Whether git username exists. + */ + public boolean hasGitUsername(GitUsername gitUsername) { + requireNonNull(gitUsername); + return persons.containsUsername(gitUsername); + } + /** * Adds a person to the address book. * The person must not already exist in the address book. diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..f5d9b139d15 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -5,7 +5,11 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.person.Email; +import seedu.address.model.person.GitUsername; import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.task.Task; /** * The API of the Model component. @@ -14,6 +18,15 @@ public interface Model { /** {@code Predicate} that always evaluate to true */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_TASKS = unused -> true; + + /** {@code Predicate} that evaluate to false if task is unmark */ + Predicate PREDICATE_SHOW_ALL_UNMARK_TASKS = task -> !task.isTaskMark(); + + /** {@code Predicate} that evaluate to true if task is mark */ + Predicate PREDICATE_SHOW_ALL_MARK_TASKS = task -> task.isTaskMark(); + /** * Replaces user prefs data with the data in {@code userPrefs}. */ @@ -53,13 +66,38 @@ public interface Model { ReadOnlyAddressBook getAddressBook(); /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if the same person as {@code Person} exists in NUS Classes */ boolean hasPerson(Person person); /** - * Deletes the given person. - * The person must exist in the address book. + * Returns true if a person with the same gitUsername as {@code gitUsername} exists in NUS Classes + * + * @param gitUsername + * @return Whether username exists + */ + boolean hasUsername(GitUsername gitUsername); + + /** + * Checks if Model has Email already existing. + * + * @param email Email to be checked. + * @return Whether email exists + */ + boolean hasEmail(Email email); + + /** + * Checks if Model has Phone already existing. + * + * @param phone Phone to be checked. + * @return Whether phone exists. + */ + boolean hasPhone(Phone phone); + + /** + * Deletes person. + * + * @param target Person to be deleted. */ void deletePerson(Person target); @@ -84,4 +122,55 @@ public interface Model { * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + ReadOnlyTaskList getTaskList(); + + /** Returns an unmodifiable view of the filtered task list */ + ObservableList getFilteredTaskList(); + + /** + * Updates the filter of the filtered task list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredTaskList(Predicate predicate); + + /** + * Adds the task to the taskList. + * + * @param task the task to be added + */ + void addTask(Task task); + + /** + * Deletes the given task. + * The task must exist in the task list. + */ + void deleteTask(Task target); + + void setTask(Task taskToEdit, Task editedTask); + + /** + * Returns true if a task with the same description as {@code task} exists in the task list. + */ + boolean hasTask(Task task); + + /** + * Marks the given task as completed. + * + * @param task the task to be marked. + */ + void markTask(Task task); + + /** + * Unmarks the given task as not complete. + * + * @param task the task to be unmarked. + */ + void unmarkTask(Task task); + + /** + * Replaces Task list data with the data in {@code tasklist}. + * + */ + void setTaskList(ReadOnlyTaskList taskList); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 86c1df298d7..b6527dc8102 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -11,7 +11,11 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.model.person.Email; +import seedu.address.model.person.GitUsername; import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.task.Task; /** * Represents the in-memory model of the address book data. @@ -22,53 +26,75 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final FilteredList filteredTasks; + private final TaskList taskList; /** * Initializes a ModelManager with the given addressBook and userPrefs. */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { - requireAllNonNull(addressBook, userPrefs); + public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs, ReadOnlyTaskList taskList) { + requireAllNonNull(addressBook, userPrefs, taskList); logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); + this.taskList = new TaskList(taskList); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredTasks = new FilteredList<>(this.taskList.getTaskList()); } public ModelManager() { - this(new AddressBook(), new UserPrefs()); + this(new AddressBook(), new UserPrefs(), new TaskList()); } //=========== UserPrefs ================================================================================== + /** + * {@inheritDoc} + */ @Override public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { requireNonNull(userPrefs); this.userPrefs.resetData(userPrefs); } + /** + * {@inheritDoc} + */ @Override public ReadOnlyUserPrefs getUserPrefs() { return userPrefs; } + /** + * {@inheritDoc} + */ @Override public GuiSettings getGuiSettings() { return userPrefs.getGuiSettings(); } + /** + * {@inheritDoc} + */ @Override public void setGuiSettings(GuiSettings guiSettings) { requireNonNull(guiSettings); userPrefs.setGuiSettings(guiSettings); } + /** + * {@inheritDoc} + */ @Override public Path getAddressBookFilePath() { return userPrefs.getAddressBookFilePath(); } + /** + * {@inheritDoc} + */ @Override public void setAddressBookFilePath(Path addressBookFilePath) { requireNonNull(addressBookFilePath); @@ -77,38 +103,85 @@ public void setAddressBookFilePath(Path addressBookFilePath) { //=========== AddressBook ================================================================================ + /** + * {@inheritDoc} + */ @Override public void setAddressBook(ReadOnlyAddressBook addressBook) { this.addressBook.resetData(addressBook); } + /** + * {@inheritDoc} + */ @Override public ReadOnlyAddressBook getAddressBook() { return addressBook; } + /** + * {@inheritDoc} + */ @Override public boolean hasPerson(Person person) { requireNonNull(person); return addressBook.hasPerson(person); } + /** + * {@inheritDoc} + */ + @Override + public boolean hasUsername(GitUsername gitUsername) { + requireNonNull(gitUsername); + return addressBook.hasUsername(gitUsername); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hasEmail(Email email) { + requireNonNull(email); + return addressBook.hasEmail(email); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hasPhone(Phone phone) { + requireNonNull(phone); + return addressBook.hasPhone(phone); + } + + /** + * {@inheritDoc} + */ @Override public void deletePerson(Person target) { addressBook.removePerson(target); + taskList.removePerson(target); } + /** + * {@inheritDoc} + */ @Override public void addPerson(Person person) { addressBook.addPerson(person); updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); } + /** + * {@inheritDoc} + */ @Override public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); addressBook.setPerson(target, editedPerson); + taskList.setPerson(target, editedPerson); } //=========== Filtered Person List Accessors ============================================================= @@ -122,6 +195,9 @@ public ObservableList getFilteredPersonList() { return filteredPersons; } + /** + * {@inheritDoc} + */ @Override public void updateFilteredPersonList(Predicate predicate) { requireNonNull(predicate); @@ -147,4 +223,87 @@ public boolean equals(Object obj) { && filteredPersons.equals(other.filteredPersons); } + //=========== TaskList ================================================================================ + + /** + * {@inheritDoc} + */ + @Override + public void setTaskList(ReadOnlyTaskList taskList) { + this.taskList.resetData(taskList); + } + + /** + * {@inheritDoc} + */ + @Override + public ReadOnlyTaskList getTaskList() { + return taskList; + } + + /** + * Returns an unmodifiable view of the list of {@code Task} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList getFilteredTaskList() { + return filteredTasks; + } + + /** + * {@inheritDoc} + */ + @Override + public void updateFilteredTaskList(Predicate predicate) { + requireNonNull(predicate); + filteredTasks.setPredicate(predicate); + } + + @Override + public void addTask(Task task) { + taskList.addTask(task); + } + + /** + * {@inheritDoc} + */ + @Override + public void deleteTask(Task target) { + taskList.deleteCurrTask(target); + } + + /** + * {@inheritDoc} + */ + @Override + public void setTask(Task target, Task editedTask) { + requireAllNonNull(target, editedTask); + + taskList.setTask(target, editedTask); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hasTask(Task task) { + requireNonNull(task); + return taskList.hasTask(task); + } + + /** + * {@inheritDoc} + */ + @Override + public void markTask(Task task) { + taskList.markTask(task); + } + + /** + * {@inheritDoc} + */ + @Override + public void unmarkTask(Task task) { + taskList.unmarkTask(task); + } } diff --git a/src/main/java/seedu/address/model/ReadOnlyTaskList.java b/src/main/java/seedu/address/model/ReadOnlyTaskList.java new file mode 100644 index 00000000000..a5aa0de5a5d --- /dev/null +++ b/src/main/java/seedu/address/model/ReadOnlyTaskList.java @@ -0,0 +1,14 @@ +package seedu.address.model; + +import javafx.collections.ObservableList; +import seedu.address.model.task.Task; + +public interface ReadOnlyTaskList { + + /** + * Returns ObservableList object containing Tasks. + * + * @return ObservableList of Task objects representing a TaskList. + */ + ObservableList getTaskList(); +} diff --git a/src/main/java/seedu/address/model/TaskList.java b/src/main/java/seedu/address/model/TaskList.java new file mode 100644 index 00000000000..a91c7594cf1 --- /dev/null +++ b/src/main/java/seedu/address/model/TaskList.java @@ -0,0 +1,182 @@ +package seedu.address.model; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.person.Person; +import seedu.address.model.task.Task; +import seedu.address.model.task.exceptions.TaskNotFoundException; + +/** + * Contains a list of Tasks + */ +public class TaskList implements Iterable, ReadOnlyTaskList { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + public TaskList() {} + + /** + * Creates a TaskList using the Tasks in the {@code toBeCopied} + */ + public TaskList(ReadOnlyTaskList toBeCopied) { + this(); + resetData(toBeCopied); + } + + /** + * Replaces the contents of the task list with {@code tasks}. + * {@code tasks} must not contain duplicate tasks. //todo: really cannot contain duplicates? + */ + public void setTasks(List tasks) { + requireAllNonNull(tasks); + internalList.setAll(tasks); + } + + + /** + * Resets the existing data of this {@code TaskList} with {@code newData}. + */ + public void resetData(ReadOnlyTaskList newData) { + requireNonNull(newData); + + setTasks(newData.getTaskList()); + } + + + /** + * Adds a Task to the list. + * + * @param taskToAdd Task to be added. + */ + public void addTask(Task taskToAdd) { + requireNonNull(taskToAdd); + this.internalList.add(taskToAdd); + } + + + @Override + public String toString() { + String output = ""; + for (int i = 0; i < this.internalList.size(); i++) { + output += this.internalList.get(i) + "\n"; + } + return output; + } + + @Override + public ObservableList getTaskList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + /** + * Deletes a Task to the list. + * + * @param taskToDelete Task to be deleted. + */ + public void deleteCurrTask(Task taskToDelete) { + requireNonNull(taskToDelete); + this.internalList.remove(taskToDelete); + } + + /** + * Replaces Task at index target in TaskList with editedTask. + * + * @param target index of task to be changed. + * @param editedTask New task to replace the previous one. + */ + public void setTask(Task target, Task editedTask) { + requireAllNonNull(target, editedTask); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new TaskNotFoundException(); + } + + internalList.set(index, editedTask); + } + + /** + * Returns true if a task with the same description as {@code task} exists in the task list. + */ + public boolean hasTask(Task task) { + requireNonNull(task); + return internalList.contains(task); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TaskList // instanceof handles nulls + && internalList.equals(((TaskList) other).internalList)); + + } + + /** + * Removes the person from each task if the task contains the person. + * @param target the person to be removed. + */ + public void removePerson(Person target) { + internalList.forEach(task -> task.removePerson(target)); + } + + + /** + * Updates the person in each task if the task contains the person. + * + * @param target the person to be updated. + * @param editedPerson the updated person. + */ + public void setPerson(Person target, Person editedPerson) { + internalList.forEach(task-> task.updatePerson(target, editedPerson)); + } + + /** + * Marks the task as completed and update the task list. + * + * @param task the task to be marked. + */ + public void markTask(Task task) { + requireAllNonNull(task); + + int index = internalList.indexOf(task); + if (index == -1) { + throw new TaskNotFoundException(); + } + + Task newTask = internalList.get(index); + newTask.markTask(); + setTask(task, newTask); + } + + /** + * Unmarks the task as not complete and update the task list. + * + * @param task the task to be unmarked. + */ + public void unmarkTask(Task task) { + requireAllNonNull(task); + + int index = internalList.indexOf(task); + if (index == -1) { + throw new TaskNotFoundException(); + } + + Task newTask = internalList.get(index); + newTask.unmarkTask(); + setTask(task, newTask); + } + +} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 25a5fd6eab9..82b5b46c164 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -15,6 +15,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path taskListFilePath = Paths.get("data", "tasklist.json"); /** * Creates a {@code UserPrefs} with default values. @@ -38,19 +39,31 @@ public void resetData(ReadOnlyUserPrefs newUserPrefs) { setAddressBookFilePath(newUserPrefs.getAddressBookFilePath()); } + /** + * Returns the GUI settings. + */ public GuiSettings getGuiSettings() { return guiSettings; } + /** + * Sets the GUI setting with {@code guiSettings}. + */ public void setGuiSettings(GuiSettings guiSettings) { requireNonNull(guiSettings); this.guiSettings = guiSettings; } + /** + * Returns the path of address book. + */ public Path getAddressBookFilePath() { return addressBookFilePath; } + /** + * Sets the path of address book to {@code addressBookFilePath}. + */ public void setAddressBookFilePath(Path addressBookFilePath) { requireNonNull(addressBookFilePath); this.addressBookFilePath = addressBookFilePath; @@ -84,4 +97,10 @@ public String toString() { return sb.toString(); } + /** + * Returns the path of task list. + */ + public Path getTaskListFilePath() { + return taskListFilePath; + } } diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index 60472ca22a0..00000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,57 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String VALIDATION_REGEX = "[^\\s].*"; - - public final String value; - - /** - * Constructs an {@code Address}. - * - * @param address A valid address. - */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; - } - - /** - * Returns true if a given string is a valid email. - */ - public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Address // instanceof handles nulls - && value.equals(((Address) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index f866e7133de..762f531f311 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -9,6 +9,8 @@ */ public class Email { + private static final int MAX_LENGTH = 54; + private static final int MIN_LENGTH = 3; private static final String SPECIAL_CHARACTERS = "+_.-"; public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " + "and adhere to the following constraints:\n" @@ -20,7 +22,9 @@ public class Email { + "The domain name must:\n" + " - end with a domain label at least 2 characters long\n" + " - have each domain label start and end with alphanumeric characters\n" - + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any."; + + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any." + + " 3. The total length of the E-mail must be at least " + MIN_LENGTH + + " and at most " + MAX_LENGTH + "characters long"; // alphanumeric and special characters private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]+"; // alphanumeric characters except underscore private static final String LOCAL_PART_REGEX = "^" + ALPHANUMERIC_NO_UNDERSCORE + "([" + SPECIAL_CHARACTERS + "]" @@ -30,7 +34,6 @@ public class Email { private static final String DOMAIN_LAST_PART_REGEX = "(" + DOMAIN_PART_REGEX + "){2,}$"; // At least two chars private static final String DOMAIN_REGEX = "(" + DOMAIN_PART_REGEX + "\\.)*" + DOMAIN_LAST_PART_REGEX; public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" + DOMAIN_REGEX; - public final String value; /** @@ -51,6 +54,13 @@ public static boolean isValidEmail(String test) { return test.matches(VALIDATION_REGEX); } + /** + * Returns if a given string is a valid length. + */ + public static boolean isValidLength(String test) { + return (test.length() >= MIN_LENGTH && test.length() <= MAX_LENGTH); + } + @Override public String toString() { return value; diff --git a/src/main/java/seedu/address/model/person/GitUsername.java b/src/main/java/seedu/address/model/person/GitUsername.java new file mode 100644 index 00000000000..4063f8c58ac --- /dev/null +++ b/src/main/java/seedu/address/model/person/GitUsername.java @@ -0,0 +1,61 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +/** + * GitUsername represents the Github userID of a Person. + */ +public class GitUsername { + + //Credits to Stack Overflow. Accreditation done in PPP + public static final String GIT_USERNAME_REGEX = "^[a-zA-Z0-9]+(-[a-zA-Z0-9]+){0,2}$"; + + public static final String MESSAGE_CONSTRAINTS = "Github usernames should only contain alphanumeric" + + " characters or single hyphens, and cannot begin or end with a hyphen."; + + private String userid; + + /** + * Constructs a Github GitUsername for Person. + * + * @param userid String username + */ + public GitUsername(String userid) { + requireNonNull(userid); + this.userid = userid; + } + + /** + * Returns username + * + * @return Github username of Person + */ + public String getUsername() { + return this.userid; + } + + /** + * Checks if Id is a valid Id. Id must be alphanumeric without a space in front. + * + * @param test Id being checked + * @return True for valid id, false for invalid id. + */ + public static boolean isValidId(String test) { + return test.matches(GIT_USERNAME_REGEX); + } + + @Override + public String toString() { + return this.userid; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof GitUsername)) { + return false; + } + GitUsername otherGitUsername = (GitUsername) other; + return this.userid.equals(otherGitUsername.userid); + } + +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 79244d71cf7..ba4a8506508 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -10,13 +10,15 @@ public class Name { public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + "A name should only contain alphabets, valid symbols " + + "(commas, full stops, slashes, apostrophes and hyphens)" + + " and it should not be blank"; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "[a-zA-z,./'-][a-zA-z ,./'-]*"; public final String fullName; diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java index c9b5868427c..4786d89535d 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java @@ -4,6 +4,7 @@ import java.util.function.Predicate; import seedu.address.commons.util.StringUtil; +import seedu.address.model.tag.Tag; /** * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. @@ -17,6 +18,21 @@ public NameContainsKeywordsPredicate(List keywords) { @Override public boolean test(Person person) { + boolean isEqual = false; + for (String str: keywords) { + Tag[] tags = new Tag[person.getTags().size()]; + person.getTags().toArray(tags); + for (int i = 0; i < tags.length; i++) { + if (StringUtil.containsWordIgnoreCase(tags[i].toString(), str.trim())) { + isEqual = true; + break; + } + } + if (isEqual) { + return isEqual; + } + } + return keywords.stream() .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); } diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index 8ff1d83fe89..17f1bcb57f7 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -6,6 +6,7 @@ import java.util.HashSet; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import seedu.address.model.tag.Tag; @@ -19,23 +20,31 @@ public class Person { private final Name name; private final Phone phone; private final Email email; + private final GitUsername gitUsername; // Data fields - private final Address address; private final Set tags = new HashSet<>(); + /** - * Every field must be present and not null. + * Constructs a Person object using 5 fields: name, Phone, Email, Address, and any number of tags. + * + * @param name Name of Person. + * @param phone Phone Number of Person. + * @param email Email address of Person. + * @param gitUsername Github GitUsername of Person. + * @param tags Tags for Person. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Phone phone, Email email, GitUsername gitUsername, Set tags) { + requireAllNonNull(name, phone, email, gitUsername, tags); this.name = name; this.phone = phone; this.email = email; - this.address = address; + this.gitUsername = gitUsername; this.tags.addAll(tags); } + public Name getName() { return name; } @@ -48,8 +57,8 @@ public Email getEmail() { return email; } - public Address getAddress() { - return address; + public GitUsername getUsername() { + return this.gitUsername; } /** @@ -61,7 +70,7 @@ public Set getTags() { } /** - * Returns true if both persons have the same name. + * Returns true if both persons are the same object. * This defines a weaker notion of equality between two persons. */ public boolean isSamePerson(Person otherPerson) { @@ -70,7 +79,7 @@ public boolean isSamePerson(Person otherPerson) { } return otherPerson != null - && otherPerson.getName().equals(getName()); + && otherPerson.equals(this); } /** @@ -88,17 +97,16 @@ public boolean equals(Object other) { } Person otherPerson = (Person) other; - return otherPerson.getName().equals(getName()) - && otherPerson.getPhone().equals(getPhone()) + return otherPerson.getPhone().equals(getPhone()) && otherPerson.getEmail().equals(getEmail()) - && otherPerson.getAddress().equals(getAddress()) + && otherPerson.getUsername().equals(getUsername()) && otherPerson.getTags().equals(getTags()); } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, gitUsername, tags); } @Override @@ -109,13 +117,13 @@ public String toString() { .append(getPhone()) .append("; Email: ") .append(getEmail()) - .append("; Address: ") - .append(getAddress()); + .append("; Github: ") + .append(getUsername()); Set tags = getTags(); if (!tags.isEmpty()) { - builder.append("; Tags: "); - tags.forEach(builder::append); + builder.append("; Tags: ") + .append(tags.stream().map(Tag::toString).collect(Collectors.joining(", "))); } return builder.toString(); } diff --git a/src/main/java/seedu/address/model/person/PersonContainInTask.java b/src/main/java/seedu/address/model/person/PersonContainInTask.java new file mode 100644 index 00000000000..679b182990f --- /dev/null +++ b/src/main/java/seedu/address/model/person/PersonContainInTask.java @@ -0,0 +1,33 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +/** + * Tests that a {@code Person} matches any of the person object provided. + */ +public class PersonContainInTask implements Predicate { + private final List targetPersonList; + + public PersonContainInTask(List targetPersonList) { + this.targetPersonList = targetPersonList; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean test(Person person) { + return targetPersonList.stream() + .anyMatch(targetPerson -> targetPerson.isSamePerson(person)); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object other) { + return other == this; + } + +} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index 872c76b382f..6fa0beda8f6 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -11,8 +11,9 @@ public class Phone { public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String VALIDATION_REGEX = "\\d{3,}"; + "Phone numbers should only contain numbers, and it should be at least " + + "3 digits long and at most 15 digits long."; + public static final String VALIDATION_REGEX = "\\d{3,15}"; public final String value; /** diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index 0fee4fe57e6..0a5ef26fec2 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -36,6 +36,54 @@ public boolean contains(Person toCheck) { return internalList.stream().anyMatch(toCheck::isSamePerson); } + /** + * Returns true if the list contains a Person with an identical GitUsername as the given argument. + * + * @param toCheck Github username to be checked. + * @return true/false whether username already exists. + */ + public boolean containsUsername(GitUsername toCheck) { + requireNonNull(toCheck); + for (int i = 0; i < internalList.size(); i++) { + if (internalList.get(i).getUsername().equals(toCheck)) { + return true; + } + } + return false; + } + + /** + * Returns true if the list contains a person with an identical Email. + * + * @param toCheck Email to be checked. + * @return Whether email already exists. + */ + public boolean containsEmail(Email toCheck) { + requireNonNull(toCheck); + for (int i = 0; i < internalList.size(); i++) { + if (internalList.get(i).getEmail().equals(toCheck)) { + return true; + } + } + return false; + } + + /** + * Returns true if the list contains a person with an identical phone. + * + * @param toCheck Phone to be checked. + * @return Whether Phone already exists. + */ + public boolean containsPhone(Phone toCheck) { + requireNonNull(toCheck); + for (int i = 0; i < internalList.size(); i++) { + if (internalList.get(i).getPhone().equals(toCheck)) { + return true; + } + } + return false; + } + /** * Adds a person to the list. * The person must not already exist in the list. diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index b0ea7e7dad7..89a2437825d 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -9,8 +9,11 @@ */ public class Tag { - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + public static final String MESSAGE_CONSTRAINTS = + "Tag names should contain ASCII characters and should not be empty"; + + //Format for valid tag name + public static final String VALIDATION_REGEX = "\\p{ASCII}+"; public final String tagName; @@ -26,10 +29,13 @@ public Tag(String tagName) { } /** - * Returns true if a given string is a valid tag name. + * Returns true if a String is a valid tag name. Tag must be alphanumeric. + * + * @param testString String to be tested. + * @return boolean true/false on whether tag name is valid. */ - public static boolean isValidTagName(String test) { - return test.matches(VALIDATION_REGEX); + public static boolean isValidTagName(String testString) { + return testString.matches(VALIDATION_REGEX); } @Override @@ -48,7 +54,7 @@ public int hashCode() { * Format state as text for viewing. */ public String toString() { - return '[' + tagName + ']'; + return tagName; } } diff --git a/src/main/java/seedu/address/model/task/Link.java b/src/main/java/seedu/address/model/task/Link.java new file mode 100644 index 00000000000..1c890ef7f41 --- /dev/null +++ b/src/main/java/seedu/address/model/task/Link.java @@ -0,0 +1,61 @@ +package seedu.address.model.task; + +import static java.util.Objects.requireNonNull; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * Represents a link associated with the task. + */ +public class Link { + public static final String MESSAGE_CONSTRAINTS = "The link provided should follows the proper URL format " + + "(must have reference to 'https://' or 'http://'. E.g. www.google.com will not be considered a valid URL" + + " whereas https://www.google.com will be considered a valid URL."; + private String link; + + /** + * Constructs a {@code Link}. + * + * @param link A link + */ + public Link(String link) { + requireNonNull(link); + + this.link = link; + } + + public Link() { + this.link = null; + } + + /** + * Checks if link is valid. + * @param link Link of users. + * @throws IllegalArgumentException If link is not a valid URL. + */ + public static boolean isValidLink(String link) { + try { + new URL(link).toURI(); + } catch (URISyntaxException | MalformedURLException e) { + // Unconventional, but currently only way to check whether a link is valid. + return false; + } + return true; + } + + @Override + public boolean equals(Object other) { + return other == this || this.link.equals(((Link) other).link); + } + + @Override + public String toString() { + return link; + } + + public boolean isEmpty() { + return link == null; + } +} 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..c7aac94e391 --- /dev/null +++ b/src/main/java/seedu/address/model/task/Task.java @@ -0,0 +1,371 @@ +package seedu.address.model.task; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Represents a Task in NUSClasses. + * Task consists of a String object representing a name and a LocalDateTime object representing the date and time. + */ +public class Task { + public static final DateTimeFormatter FORMAT_TIME = DateTimeFormatter.ofPattern("h.mm a"); + public static final DateTimeFormatter FORMAT_DAY_OF_WEEK = DateTimeFormatter.ofPattern("EEE, h.mm a"); + public static final DateTimeFormatter FORMAT_MONTH = DateTimeFormatter.ofPattern("dd MMM, h.mm a"); + public static final DateTimeFormatter FORMAT_YEAR = DateTimeFormatter.ofPattern("dd MMM yyyy, h.mm a"); + private static final int MAX_LENGTH = 100; + private static final int MIN_LENGTH = 3; + public static final String NAME_LENGTH_ERROR = "The name of the tasks must be at least " + + MIN_LENGTH + " characters long and at most " + MAX_LENGTH + " characters long"; + private String name; + private LocalDateTime dateTime; + private LocalDateTime endDateTime; + private List people; + private Set tags; + private Link link; + private boolean isMarkDone; + + /** + * Constructs Task that takes in the following parameters. + * + * @param name Name of task. + * @param people People to be added to the list. + * @param dateTime LocalDateTime object representing Date and Time for Task. + * @param tags Tags for the tasks. + * @param link Link to be added to the task. + * @param isMarkDone true if task is done, else false. + */ + public Task(String name, LocalDateTime dateTime, LocalDateTime endDateTime, List people, Set tags, + Link link, boolean isMarkDone) { + this.name = name; + this.dateTime = dateTime; + this.endDateTime = endDateTime; + this.people = new ArrayList<>(people); + this.tags = tags; + this.link = link; + this.isMarkDone = isMarkDone; + } + + /** + * Constructor for Task with people but no endDateTime. + */ + public Task(String name, LocalDateTime dateTime, List people, Set tags, Link link, + boolean isMarkDone) { + this(name, dateTime, null, people, tags, link, isMarkDone); + } + + /** + * Constructs Task with endDateTime but no people. + */ + public Task(String name, LocalDateTime dateTime, LocalDateTime endDateTime, Set tags, Link link, + boolean isMarkDone) { + this(name, dateTime, endDateTime, new ArrayList<>(), tags, link, isMarkDone); + } + + /** + * Constructs Task without people or endDateTime. + */ + public Task(String name, LocalDateTime dateTime, Set tags, Link link, boolean isMarkDone) { + this(name, dateTime, null, new ArrayList<>(), tags, link, isMarkDone); + } + + + + /** + * Changes name of Task. + * + * @param name new Name to be changed. + */ + public void changeName(String name) { + this.name = name; + } + + /** + * Adds a person to the list of people associated with the task. + * + * @param person Person to add. + */ + public void addPerson(Person person) { + people.add(person); + } + + /** + * Removes a person from the list of people associated with the task. + * + * @param person Person to remove. + */ + public void removePerson(Person person) { + people.remove(person); + } + + /** + * Updates a person in the list of people associated with the task. + * + * @param person The person to update. + * @param editedPerson The edited person. + */ + public void updatePerson(Person person, Person editedPerson) { + int index = people.indexOf(person); + if (index != -1) { + people.set(index, editedPerson); + } + } + + /** + * Changes DateTime of Task. + * + * @param newDateTime LocalDateTime of new DateTime. + */ + public void changeDateTime(LocalDateTime newDateTime) { + this.dateTime = newDateTime; + } + + /** + * Changes EndDateTime of Task. + * + * @param endDateTime LocalDateTime of new endDateTime. + */ + public void changeEndDateTime(LocalDateTime endDateTime) { + this.endDateTime = endDateTime; + } + + @Override + public String toString() { + return this.name + " " + dateTime.format(FORMAT_YEAR); + } + + /** + * Checks if this task has an end date time or not. + * @return true if this task has an end date time, false otherwise. + */ + public boolean hasEndDateTime() { + return this.endDateTime != null; + } + + /** + * Returns a user-friendly representation of the dateTime and endDateTime. + */ + public String getDateTimeString() { + if (endDateTime == null) { + return getDeadline(); + } + return getDateTimeRange(); + } + + private String getDeadline() { + return String.format("Due: %s", getUserFriendlyDateTime(dateTime)); + } + + private String getDateTimeRange() { + assert endDateTime != null; + LocalDateTime date = dateTime.toLocalDate().atStartOfDay(); + LocalDateTime endDate = endDateTime.toLocalDate().atStartOfDay(); + + String result; + if (Duration.between(date, endDate).toDays() == 0) { + result = String.format("%s - %s", getUserFriendlyDateTime(dateTime), endDateTime.format(FORMAT_TIME)); + } else { + result = String.format("%s - %s", getUserFriendlyDateTime(dateTime), getUserFriendlyDateTime(endDateTime)); + } + return String.format("From: %s", result); + } + + /** + * Methods for generating date representation relative to current calendar day. + * + * @param dateTime The dateTime to be processed. + * @return User-friendly dateTime string. + */ + static String getUserFriendlyDateTime(LocalDateTime dateTime) { + LocalDateTime today = LocalDate.now().atStartOfDay(); + LocalDateTime date = dateTime.toLocalDate().atStartOfDay(); + long daysFrom = Duration.between(today, date).toDays(); + String result; + if (daysFrom == 0) { // today + result = String.format("Today, %s", dateTime.format(FORMAT_TIME)); + } else if (daysFrom == 1) { // tomorrow + result = String.format("Tomorrow, %s", dateTime.format(FORMAT_TIME)); + } else if (daysFrom >= 2 && daysFrom <= 7) { // this week + result = dateTime.format(FORMAT_DAY_OF_WEEK); + } else if (date.getYear() == today.getYear()) { // this year + result = dateTime.format(FORMAT_MONTH); + } else { // next year onwards + result = dateTime.format(FORMAT_YEAR); + } + return result; + } + + + /** + * Returns a user-friendly representation of the endDateTime. + */ + public String getEndDateTimeString() { + return this.endDateTime.format(FORMAT_YEAR); + } + + + /** + * Returns DateTime of Task. + * + * @return DateTime object of Task. + */ + public LocalDateTime getDateTime() { + return this.dateTime; + } + + /** + * Returns endDateTime of Task. + * + * @return endDateTime object of Task. + */ + public LocalDateTime getEndDateTime() { + return this.endDateTime; + } + + + /** + * Returns name of Task. + * + * @return Name of Task. + */ + public String getName() { + return this.name; + } + + /** + * Returns Tag of Task. + * + * @return Tag of Task. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + /** + * Sets new tags for Task object + * + * @param tags new Set of tags to replace previous ones + */ + public void setTags(Set tags) { + this.tags = tags; + } + + /** + * Returns list of People assigned to Task. + * + * @return List of People. + */ + public List getPeople() { + return this.people; + } + + /** + * Returns true if both tasks have the same name. + */ + public boolean isSameTask(Task otherTask) { + if (otherTask == this) { + return true; + } + return otherTask != null + && otherTask.getName().equals(getName()); + } + + /** + * Returns the number of people assigned to Task. + * + * @return Number of people. + */ + public int getNoOfPeople() { + return this.people.size(); + } + + /** + * Returns a copy-paste friendly string containing all the emails related to this task. + * The emails will be joined with a comma separator (e.g. "e1234578@u.nus.edu.sg, e12121212@u.nus.edu.sg"). + * + * @return The generated email string + */ + public String getEmails() { + String[] emails = this.people.stream().map(p -> p.getEmail().toString()).toArray(String[]::new); + return String.join(", ", emails); + } + + /** + * Checks if this task contains the person. + * @param p the person to check for + * @return true if this task contains the person, false otherwise. + */ + public boolean containsPerson(Person p) { + return this.people.contains(p); + } + + /** + * Returns the zoom link assigned to Task. + * + * @return A link. + */ + public Link getLink() { + return link; + } + + /** + * Sets the isMarkDone as true to show that the task is done. + */ + public void markTask() { + this.isMarkDone = true; + } + + /** + * Sets the isMarkDone as false to show that the task is not done. + */ + public void unmarkTask() { + this.isMarkDone = false; + } + + /** + * Returns the status if the task is mark done. + */ + public boolean isTaskMark() { + return isMarkDone; + } + + /** + * Returns true if endDateTime is earlier than dateTime + * + * @return True if endDateTime is earlier, false if endDateTime is later + */ + public boolean hasInvalidDateRange() { + return endDateTime != null && dateTime.compareTo(endDateTime) >= 0; + } + + /** + * Returns if a given string is a valid length. + */ + public static boolean isValidLength(String testString) { + return (testString.length() >= MIN_LENGTH && testString.length() <= MAX_LENGTH); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Task)) { + return false; + } + + Task otherTask = (Task) other; + return otherTask.getName().equals(this.getName()) + && otherTask.getDateTime().equals(this.getDateTime()) + && otherTask.getPeople().equals(this.getPeople()); + } +} diff --git a/src/main/java/seedu/address/model/task/TaskBetweenDatesPredicate.java b/src/main/java/seedu/address/model/task/TaskBetweenDatesPredicate.java new file mode 100644 index 00000000000..3a1a80e7e54 --- /dev/null +++ b/src/main/java/seedu/address/model/task/TaskBetweenDatesPredicate.java @@ -0,0 +1,67 @@ +package seedu.address.model.task; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.function.Predicate; + +/** + * Tests that a {@code Task}'s {@code Name} matches any of the keywords given. + */ +public class TaskBetweenDatesPredicate implements Predicate { + private final List beforeAfterDateList; + + public TaskBetweenDatesPredicate(List beforeAfterDateList) { + this.beforeAfterDateList = beforeAfterDateList; + } + + @Override + public boolean test(Task task) { + if (task.getEndDateTime() == null) { + boolean isTaskDateAfterMinDateRange = task.getDateTime().isAfter(beforeAfterDateList.get(0)); + boolean isTaskDateBeforeMaxDateRange = task.getDateTime().isBefore(beforeAfterDateList.get(1)); + boolean isTaskDateOnMinDateRange = task.getDateTime().isEqual(beforeAfterDateList.get(0)); + boolean isTaskDateOnMaxDateRange = task.getDateTime().isEqual(beforeAfterDateList.get(1)); + + boolean isTaskDateValid = isTaskDateAfterMinDateRange && isTaskDateBeforeMaxDateRange; + return (isTaskDateValid || isTaskDateOnMinDateRange || isTaskDateOnMaxDateRange); + + } else { + boolean isTaskDateAfterMinDateRange = task.getDateTime().isAfter(beforeAfterDateList.get(0)); + boolean isTaskDateBeforeMaxDateRange = task.getDateTime().isBefore(beforeAfterDateList.get(1)); + boolean isTaskDateOnMinDateRange = task.getDateTime().isEqual(beforeAfterDateList.get(0)); + boolean isTaskDateOnMaxDateRange = task.getDateTime().isEqual(beforeAfterDateList.get(1)); + boolean isTaskEndDateAfterMinDateRange = task.getEndDateTime().isAfter(beforeAfterDateList.get(0)); + boolean isTaskEndDateBeforeMaxDateRange = task.getEndDateTime().isBefore(beforeAfterDateList.get(1)); + + boolean isTaskDateValid = isTaskDateAfterMinDateRange && isTaskDateBeforeMaxDateRange; + boolean isTaskEndDateValid = isTaskEndDateAfterMinDateRange && isTaskEndDateBeforeMaxDateRange; + + return (isTaskDateValid || isTaskEndDateValid || isTaskDateOnMinDateRange || isTaskDateOnMaxDateRange); + } + + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TaskBetweenDatesPredicate // instanceof handles nulls + && areDatesEqual(this.beforeAfterDateList, ((TaskBetweenDatesPredicate) other) + .beforeAfterDateList)); // state check + } + + /** + * Checks if 2 list of dates are the same date. + * + * @param list1 First list of dates. + * @param list2 Second list of dates. + * @return True if both list have the same dates. + */ + private boolean areDatesEqual(List list1, List list2) { + boolean toReturn = false; + for (int i = 0; i < list1.size(); i++) { + toReturn = list1.get(i).isEqual(list2.get(i)); + } + return toReturn; + } + +} diff --git a/src/main/java/seedu/address/model/task/TaskNameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/task/TaskNameContainsKeywordsPredicate.java new file mode 100644 index 00000000000..aab111ddfb2 --- /dev/null +++ b/src/main/java/seedu/address/model/task/TaskNameContainsKeywordsPredicate.java @@ -0,0 +1,63 @@ +package seedu.address.model.task; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.model.tag.Tag; + +/** + * Tests that a {@code Task}'s {@code Name} matches any of the keywords given. + */ +public class TaskNameContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public TaskNameContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Task task) { + boolean isEqual = false; + for (String str: keywords) { + if (StringUtil.containsWordIgnoreCase(task.getName(), str.trim())) { + isEqual = true; + break; + } + Tag[] tagsArr = new Tag[task.getTags().size()]; + task.getTags().toArray(tagsArr); + for (int i = 0; i < tagsArr.length; i++) { + if (StringUtil.containsWordIgnoreCase(tagsArr[i].toString(), str.trim())) { + isEqual = true; + break; + } + } + if (isEqual) { + break; + } + } + return isEqual; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TaskNameContainsKeywordsPredicate // instanceof handles nulls + && areKeywordsEqual(this.keywords, ((TaskNameContainsKeywordsPredicate) other) + .keywords)); // state check + } + + private boolean areKeywordsEqual(List list1, List list2) { + if (list1.size() == list2.size()) { + boolean toReturn = false; + for (int i = 0; i < list1.size(); i++) { + toReturn = list1.get(i).equals(list2.get(i)); + } + return toReturn; + } else { + return false; + } + + } + +} 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..0a4c154f4ea --- /dev/null +++ b/src/main/java/seedu/address/model/task/exceptions/TaskNotFoundException.java @@ -0,0 +1,3 @@ +package seedu.address.model.task.exceptions; + +public class TaskNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..e05a327ecf2 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,17 +1,22 @@ package seedu.address.model.util; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; +import seedu.address.model.ReadOnlyTaskList; +import seedu.address.model.TaskList; import seedu.address.model.person.Email; +import seedu.address.model.person.GitUsername; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; +import seedu.address.model.task.Link; +import seedu.address.model.task.Task; /** * Contains utility methods for populating {@code AddressBook} with sample data. @@ -19,27 +24,30 @@ public class SampleDataUtil { public static Person[] getSamplePersons() { return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + new Person(new Name("Joseph"), new Phone("89993233"), new Email("Joseph@nus.edu.sg"), + new GitUsername("ProfJosephNUS"), getTagSet("Colleague", "Lecturer")), + new Person(new Name("Example TA"), new Phone("92624417"), new Email("e111111@u.nus.edu"), + new GitUsername("TACS2103T"), + getTagSet("TA", "T-12")), + new Person(new Name("Brian Chow"), new Phone("87438807"), new Email("e123456@u.nus.edu"), + new GitUsername("brian16600"), + getTagSet("CS2101 Group 4", "CS2103T Lab 12")), + new Person(new Name("Sean Ng"), new Phone("99272758"), new Email("e234567@u.nus.edu"), + new GitUsername("snss231"), + getTagSet("Teammate", "CS2101 Group 4")), + new Person(new Name("Adrian Ong"), new Phone("93210283"), new Email("e345678@u.nus.edu"), + new GitUsername("adrianongjj"), + getTagSet("CS2103T Lab 12")), + new Person(new Name("Ong Jun Jie"), new Phone("91031282"), new Email("e456789@u.nus.edu"), + new GitUsername("junjunjieong"), + getTagSet("CS2107", "CS2040")), + new Person(new Name("Jun Rong"), new Phone("92492021"), new Email("e567890@u.nus.edu"), + new GitUsername("junrong98"), + getTagSet("CS2030", "CS2107")) }; } + public static ReadOnlyAddressBook getSampleAddressBook() { AddressBook sampleAb = new AddressBook(); for (Person samplePerson : getSamplePersons()) { @@ -57,4 +65,39 @@ public static Set getTagSet(String... strings) { .collect(Collectors.toSet()); } + public static Task[] getSampleTasks() { + return new Task[] { + new Task("Meeting with TAs", LocalDateTime.now().minusDays(2), + getTagSet("Discuss tutorials"), new Link(), true), + new Task("Consultation with students", LocalDateTime.now().plusDays(1), + getTagSet("Consultation"), + new Link("https://nus-sg.zoom.us/j/92307270969?pwd=VVMvNWFPWFpyVHRIcXR0VkJlNkg0dz09"), + false), + new Task("CS2103T Lecture", + LocalDateTime.of(2050, 4, 8, 14, 0), + getTagSet("Week 12 Lecture"), + new Link("https://nus-sg.zoom.us/j/92307270969?pwd=VVMvNWFPWFpyVHRIcXR0VkJlNkg0dz09"), + false), + new Task("CS2103T Lecture", + LocalDateTime.of(2050, 4, 15, 14, 0), + getTagSet("Week 13 Lecture"), + new Link("https://nus-sg.zoom.us/j/92307270969?pwd=VVMvNWFPWFpyVHRIcXR0VkJlNkg0dz09"), false), + new Task("Meeting with exam invigilators", LocalDateTime.now().plusWeeks(2) , + getTagSet("Meeting"), new Link(), false), + new Task("CS2103T Lecture", + LocalDateTime.of(2050, 4, 22, 14, 0), + getTagSet("Week 14 Lecture"), + new Link("https://nus-sg.zoom.us/j/92307270969?pwd=VVMvNWFPWFpyVHRIcXR0VkJlNkg0dz09"), + false) + }; + } + + public static ReadOnlyTaskList getSampleTaskList() { + TaskList sampleTl = new TaskList(); + for (Task sampleTask : getSampleTasks()) { + sampleTl.addTask(sampleTask); + } + return sampleTl; + } + } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index a6321cec2ea..1caff641cd5 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -10,8 +10,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.GitUsername; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -27,7 +27,7 @@ class JsonAdaptedPerson { private final String name; private final String phone; private final String email; - private final String address; + private final String gitUsername; private final List tagged = new ArrayList<>(); /** @@ -35,12 +35,12 @@ class JsonAdaptedPerson { */ @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, + @JsonProperty("email") String email, @JsonProperty("gitUsername") String gitUsername, @JsonProperty("tagged") List tagged) { this.name = name; this.phone = phone; this.email = email; - this.address = address; + this.gitUsername = gitUsername; if (tagged != null) { this.tagged.addAll(tagged); } @@ -53,7 +53,7 @@ public JsonAdaptedPerson(Person source) { name = source.getName().fullName; phone = source.getPhone().value; email = source.getEmail().value; - address = source.getAddress().value; + gitUsername = source.getUsername().getUsername(); tagged.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); @@ -94,16 +94,17 @@ public Person toModelType() throws IllegalValueException { } final Email modelEmail = new Email(email); - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); + if (gitUsername == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + GitUsername.class.getSimpleName())); } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); + if (!GitUsername.isValidId(gitUsername)) { + throw new IllegalValueException(GitUsername.MESSAGE_CONSTRAINTS); } - final Address modelAddress = new Address(address); + final GitUsername modelGitUsername = new GitUsername(gitUsername); final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + return new Person(modelName, modelPhone, modelEmail, modelGitUsername, modelTags); } } 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..01d77c88361 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedTask.java @@ -0,0 +1,120 @@ +package seedu.address.storage; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; +import seedu.address.model.task.Link; +import seedu.address.model.task.Task; + +/** + * Jackson-friendly version of {@link Task}. + */ +public class JsonAdaptedTask { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Task's %s field is missing!"; + + private final String name; + private final String dateTime; + private final String endDateTime; + private final List tagged = new ArrayList<>(); + private final List people = new ArrayList<>(); + private final String link; + private final String isTaskMarkDone; + + /** + * Constructs a {@code JsonAdaptedTask} with the given task details. + */ + @JsonCreator + public JsonAdaptedTask(@JsonProperty("name") String name, + @JsonProperty("dateTime") String dateTime, + @JsonProperty("endDateTime") String endDateTime, + @JsonProperty("people") List people, + @JsonProperty("tagged") List tagged, + @JsonProperty("link") String link, + @JsonProperty("isTaskMarkDone") String isTaskMarkDone) { + this.name = name; + this.dateTime = dateTime; + this.endDateTime = endDateTime; + this.people.addAll(people); + this.link = link; + + if (tagged != null) { + this.tagged.addAll(tagged); + } + this.isTaskMarkDone = isTaskMarkDone; + } + + /** + * Converts a given {@code Task} into this class for Jackson use. + */ + public JsonAdaptedTask(Task source) { + name = source.getName(); + dateTime = String.valueOf(source.getDateTime()); + endDateTime = String.valueOf(source.getEndDateTime()); + link = source.getLink().toString(); + people.addAll(source.getPeople().stream() + .map(JsonAdaptedPerson::new) + .collect(Collectors.toList())); + tagged.addAll(source.getTags().stream() + .map(JsonAdaptedTag::new) + .collect(Collectors.toList())); + isTaskMarkDone = String.valueOf(source.isTaskMark()); + } + + /** + * 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 { + final List taskTags = new ArrayList<>(); + for (JsonAdaptedTag tag : tagged) { + taskTags.add(tag.toModelType()); + } + + List modelPeople = new ArrayList<>(); + for (JsonAdaptedPerson person : people) { + modelPeople.add(person.toModelType()); + } + + + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "Name")); + } + + if (dateTime == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "dateTime")); + } + LocalDateTime modelDateTime = LocalDateTime.parse(dateTime); + + LocalDateTime modelEndDateTime = Objects.equals(endDateTime, "null") + ? null : LocalDateTime.parse(endDateTime); + + Set modelTag = new HashSet<>(taskTags); + + Link modelLink = Objects.equals(link, null) ? new Link() : new Link(link); + if (!modelLink.isEmpty()) { + if (!Link.isValidLink(modelLink.toString())) { + throw new IllegalValueException(Link.MESSAGE_CONSTRAINTS); + } + } + if (isTaskMarkDone == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "isTaskMarkDone")); + } + + boolean modelIsTaskMarkDone = Boolean.parseBoolean(isTaskMarkDone); + + return new Task(name, modelDateTime, modelEndDateTime, modelPeople, modelTag, modelLink, modelIsTaskMarkDone); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java index dfab9daaa0d..293d52b80af 100644 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java @@ -36,6 +36,7 @@ public Optional readAddressBook() throws DataConversionExce return readAddressBook(filePath); } + /** * Similar to {@link #readAddressBook()}. * diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..cc814e32b92 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -20,6 +20,9 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_PHONE = "Persons list contains duplicate phone number(s)."; + public static final String MESSAGE_DUPLICATE_GIT_USERNAME = "Persons list contains duplicate git username(s)."; + public static final String MESSAGE_DUPLICATE_EMAIL = "Persons list contains duplicate git E-mail(s)."; private final List persons = new ArrayList<>(); @@ -52,6 +55,19 @@ public AddressBook toModelType() throws IllegalValueException { if (addressBook.hasPerson(person)) { throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); } + + if (addressBook.hasPhone(person.getPhone())) { + throw new IllegalValueException(MESSAGE_DUPLICATE_PHONE); + } + + if (addressBook.hasGitUsername(person.getUsername())) { + throw new IllegalValueException(MESSAGE_DUPLICATE_GIT_USERNAME); + } + + if (addressBook.hasEmail(person.getEmail())) { + throw new IllegalValueException(MESSAGE_DUPLICATE_EMAIL); + } + addressBook.addPerson(person); } return addressBook; diff --git a/src/main/java/seedu/address/storage/JsonSerializableTaskList.java b/src/main/java/seedu/address/storage/JsonSerializableTaskList.java new file mode 100644 index 00000000000..6ada0c12c87 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonSerializableTaskList.java @@ -0,0 +1,54 @@ +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.ReadOnlyTaskList; +import seedu.address.model.TaskList; +import seedu.address.model.task.Task; + +/** + * An Immutable TaskList that is serializable to JSON format. + */ +@JsonRootName(value = "tasks") +public class JsonSerializableTaskList { + + private final List tasks = new ArrayList<>(); + + /** + * Constructs a {@code JsonSerializableTasks} with the given tasks. + */ + @JsonCreator + public JsonSerializableTaskList(@JsonProperty("tasks") List tasks) { + this.tasks.addAll(tasks); + } + + /** + * Converts a given {@code ReadOnlyTasks} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableTasks}. + */ + public JsonSerializableTaskList(ReadOnlyTaskList source) { + tasks.addAll(source.getTaskList().stream().map(JsonAdaptedTask::new).collect(Collectors.toList())); + } + + /** + * Converts this address book into the model's {@code Tasks} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public TaskList toModelType() throws IllegalValueException { + TaskList taskList = new TaskList(); + for (JsonAdaptedTask jsonAdaptedTask : tasks) { + Task task = jsonAdaptedTask.toModelType(); + taskList.addTask(task); + } + return taskList; + } +} diff --git a/src/main/java/seedu/address/storage/JsonTaskListStorage.java b/src/main/java/seedu/address/storage/JsonTaskListStorage.java new file mode 100644 index 00000000000..93e47fd6542 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonTaskListStorage.java @@ -0,0 +1,104 @@ +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.DataConversionException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.FileUtil; +import seedu.address.commons.util.JsonUtil; +import seedu.address.model.ReadOnlyTaskList; +import seedu.address.model.TaskList; + + +/** + * JSON Storage for Tasks. + */ +public class JsonTaskListStorage implements TaskListStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonTaskListStorage.class); + + /* FilePath */ + private Path filePath; + + /** + * Constructor for JsonTasksStorage. + * + * @param filePath FilePath destination. + */ + public JsonTaskListStorage(Path filePath) { + this.filePath = filePath; + } + + /** + * Returns FilePath. + * + * @return FilePath of Tasks. + */ + public Path getTaskListFilePath() { + return filePath; + } + + /** + * Reads Tasks + * + * @return Optional Object containing Tasks. + * @throws DataConversionException if data is in incorrect format in filepath. + */ + public Optional readTaskList() throws DataConversionException { + return readTaskList(filePath); + } + + /** + * Reads Tasks from JSON. + * + * @param filePath location of the data. Cannot be null. + * @throws DataConversionException if the file format is not as expected. + */ + public Optional readTaskList(Path filePath) throws DataConversionException { + requireNonNull(filePath); + + Optional jsonTaskList = JsonUtil.readJsonFile( + filePath, JsonSerializableTaskList.class); + + if (!jsonTaskList.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonTaskList.get().toModelType()); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveTaskList(ReadOnlyTaskList taskList) throws IOException { + saveTaskList(taskList, filePath); + } + + @Override + public void saveTaskList(ReadOnlyTaskList taskList, Path filePath) throws IOException { + requireNonNull(taskList); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableTaskList(taskList), filePath); + } + + /** + * Saves Tasks into JSON. + * + * @param taskList Tasks object containing tasklist to be saved + * @throws IOException + */ + public void saveTasks(TaskList taskList) throws IOException { + JsonUtil.saveJsonFile(taskList, filePath); + } +} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index beda8bd9f11..38b85397315 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -6,13 +6,14 @@ import seedu.address.commons.exceptions.DataConversionException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyTaskList; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends AddressBookStorage, UserPrefsStorage, TaskListStorage { @Override Optional readUserPrefs() throws DataConversionException, IOException; @@ -26,7 +27,16 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { @Override Optional readAddressBook() throws DataConversionException, IOException; + Path getTaskListFilePath(); + + Optional readTaskList() throws DataConversionException, IOException; + + Optional readTaskList(Path filePath) throws DataConversionException, IOException; + @Override void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + void saveTaskList(ReadOnlyTaskList taskList) throws IOException; + + void saveTaskList(ReadOnlyTaskList taskList, Path filePath) throws IOException; } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index 6cfa0162164..1c2a4f69c94 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -8,6 +8,7 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.commons.exceptions.DataConversionException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyTaskList; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; @@ -17,15 +18,18 @@ public class StorageManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); + private TaskListStorage taskListStorage; private AddressBookStorage addressBookStorage; private UserPrefsStorage userPrefsStorage; /** * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}. */ - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { + public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage, + TaskListStorage taskListStorage) { this.addressBookStorage = addressBookStorage; this.userPrefsStorage = userPrefsStorage; + this.taskListStorage = taskListStorage; } // ================ UserPrefs methods ============================== @@ -75,4 +79,32 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro addressBookStorage.saveAddressBook(addressBook, filePath); } + // ================ TaskList methods ============================== + + @Override + public Path getTaskListFilePath() { + return taskListStorage.getTaskListFilePath(); + } + + @Override + public Optional readTaskList() throws DataConversionException, IOException { + return readTaskList(taskListStorage.getTaskListFilePath()); + } + + @Override + public Optional readTaskList(Path filePath) throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + return taskListStorage.readTaskList(filePath); + } + + @Override + public void saveTaskList(ReadOnlyTaskList taskList) throws IOException { + saveTaskList(taskList, taskListStorage.getTaskListFilePath()); + } + + @Override + public void saveTaskList(ReadOnlyTaskList taskList, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + taskListStorage.saveTaskList(taskList, filePath); + } } diff --git a/src/main/java/seedu/address/storage/TaskListStorage.java b/src/main/java/seedu/address/storage/TaskListStorage.java new file mode 100644 index 00000000000..d7d393a3e87 --- /dev/null +++ b/src/main/java/seedu/address/storage/TaskListStorage.java @@ -0,0 +1,44 @@ +package seedu.address.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyTaskList; + + +/** + * Represents a storage for {@link seedu.address.model.TaskList}. + */ +public interface TaskListStorage { + + Path getTaskListFilePath(); + + /** + * Returns AddressBook data as a {@link ReadOnlyAddressBook}. + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readTaskList() throws DataConversionException, IOException; + + /** + * @see #getTaskListFilePath() + */ + Optional readTaskList(Path filePath) throws DataConversionException, IOException; + + /** + * Saves the given {@link ReadOnlyTaskList} to the storage. + * @param taskList cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveTaskList(ReadOnlyTaskList taskList) throws IOException; + + /** + * @see #saveTaskList(ReadOnlyTaskList) + */ + void saveTaskList(ReadOnlyTaskList taskList, Path filePath) throws IOException; + +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 9a665915949..77dd984f04c 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -1,9 +1,16 @@ package seedu.address.ui; +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.logging.Logger; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; @@ -15,17 +22,18 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; - public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; + public static final String USERGUIDE_URL = "https://ay2122s2-cs2103t-t12-4.github.io/tp/UserGuide.html"; + public static final String HELP_MESSAGE = "Refer to the user guide: "; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); private static final String FXML = "HelpWindow.fxml"; @FXML private Button copyButton; - @FXML private Label helpMessage; + @FXML + private Hyperlink helpLink; /** * Creates a new HelpWindow. @@ -35,6 +43,17 @@ public class HelpWindow extends UiPart { public HelpWindow(Stage root) { super(FXML, root); helpMessage.setText(HELP_MESSAGE); + helpLink.setText(USERGUIDE_URL); + helpLink.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent event) { + try { + Desktop.getDesktop().browse(new URI(helpLink.getText())); + } catch (URISyntaxException | IOException e) { + helpLink.getStyleClass().add("cell_small_hyperlink_invalid"); + } + } + }); } /** diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 9106c3aa6e5..dff7cfeb9a8 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -8,6 +8,7 @@ import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import seedu.address.commons.core.GuiSettings; @@ -32,9 +33,11 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private TaskListPanel taskListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + @FXML private StackPane commandBoxPlaceholder; @@ -44,12 +47,18 @@ public class MainWindow extends UiPart { @FXML private StackPane personListPanelPlaceholder; + @FXML + private StackPane taskListPanelPlaceholder; + @FXML private StackPane resultDisplayPlaceholder; @FXML private StackPane statusbarPlaceholder; + @FXML + private HBox lists; + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ @@ -113,10 +122,13 @@ void fillInnerParts() { personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + taskListPanel = new TaskListPanel(logic.getFilteredTaskList()); + taskListPanelPlaceholder.getChildren().add(taskListPanel.getRoot()); + resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); + StatusBarFooter statusBarFooter = new StatusBarFooter(); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); CommandBox commandBox = new CommandBox(this::executeCommand); @@ -163,6 +175,11 @@ private void handleExit() { primaryStage.hide(); } + private void handleEmails(String emails) { + resultDisplay.setEmails(emails); + resultDisplay.showCopy(); + } + public PersonListPanel getPersonListPanel() { return personListPanel; } @@ -178,6 +195,12 @@ private CommandResult executeCommand(String commandText) throws CommandException logger.info("Result: " + commandResult.getFeedbackToUser()); resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + if (commandResult.isGenerateEmails()) { + handleEmails(commandResult.getEmails()); + } else { + resultDisplay.hideCopy(); + } + if (commandResult.isShowHelp()) { handleHelp(); } @@ -190,6 +213,7 @@ private CommandResult executeCommand(String commandText) throws CommandException } catch (CommandException | ParseException e) { logger.info("Invalid command: " + commandText); resultDisplay.setFeedbackToUser(e.getMessage()); + resultDisplay.hideCopy(); throw e; } } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 7fc927bc5d9..84f37575b6c 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -35,7 +35,7 @@ public class PersonCard extends UiPart { @FXML private Label phone; @FXML - private Label address; + private Label gitUsername; @FXML private Label email; @FXML @@ -47,11 +47,15 @@ public class PersonCard extends UiPart { public PersonCard(Person person, int displayedIndex) { super(FXML); this.person = person; + + gitUsername.setWrapText(true); + email.setWrapText(true); + id.setText(displayedIndex + ". "); name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); + phone.setText("Num: " + person.getPhone().value); + gitUsername.setText("Github: " + person.getUsername().getUsername()); + email.setText("Email: " + person.getEmail().value); person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/address/ui/ResultDisplay.java index 7d98e84eedf..bba5b9a219d 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/address/ui/ResultDisplay.java @@ -3,7 +3,10 @@ import static java.util.Objects.requireNonNull; import javafx.fxml.FXML; +import javafx.scene.control.Button; import javafx.scene.control.TextArea; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; import javafx.scene.layout.Region; /** @@ -13,11 +16,28 @@ public class ResultDisplay extends UiPart { private static final String FXML = "ResultDisplay.fxml"; + private static String emails; + @FXML private TextArea resultDisplay; + @FXML + private Button copyButton; + + /** + * Creates the region where feedback will be displayed. + */ public ResultDisplay() { super(FXML); + this.copyButton.setVisible(false); + } + + @FXML + private void copyEmails() { + final Clipboard clipboard = Clipboard.getSystemClipboard(); + final ClipboardContent content = new ClipboardContent(); + content.putString(emails); + clipboard.setContent(content); } public void setFeedbackToUser(String feedbackToUser) { @@ -25,4 +45,15 @@ public void setFeedbackToUser(String feedbackToUser) { resultDisplay.setText(feedbackToUser); } + public void setEmails(String emails) { + this.emails = emails; + } + + public void showCopy() { + this.copyButton.setVisible(true); + } + + public void hideCopy() { + this.copyButton.setVisible(false); + } } diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java index b577f829423..b8e7a85d669 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/StatusBarFooter.java @@ -1,8 +1,5 @@ package seedu.address.ui; -import java.nio.file.Path; -import java.nio.file.Paths; - import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.layout.Region; @@ -18,11 +15,11 @@ public class StatusBarFooter extends UiPart { private Label saveLocationStatus; /** - * Creates a {@code StatusBarFooter} with the given {@code Path}. + * Creates a {@code StatusBarFooter}. */ - public StatusBarFooter(Path saveLocation) { + public StatusBarFooter() { super(FXML); - saveLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); + saveLocationStatus.setText(""); } } 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..95a545621b1 --- /dev/null +++ b/src/main/java/seedu/address/ui/TaskCard.java @@ -0,0 +1,144 @@ +package seedu.address.ui; + +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.LocalDateTime; +import java.util.Comparator; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.task.Task; + +/** + * An UI component that displays information of a {@code Task}. + */ +public class TaskCard extends UiPart { + + private static final String FXML = "TaskListCard.fxml"; + + // Credit: Image icon taken from https://icons8.com. + private static final String TICK_ICON = "/images/tick.png"; + private static final String UNTICK_ICON = "/images/untick.png"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Task task; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label name; + @FXML + private Label date; + @FXML + private FlowPane tags; + @FXML + private Hyperlink link; + @FXML + private ImageView markImage; + @FXML + private Label linkLabel; + + /** + * Creates a {@code TaskCode} with the given {@code Task} and index to display. + */ + public TaskCard(Task task, int displayedIndex) { + super(FXML); + this.task = task; + id.setText(displayedIndex + ". "); + + name.setText(task.getName()); + setTaskColor(task.getDateTime()); + + date.setText(task.getDateTimeString()); + + task.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + + setLink(); + setMarkedImage(task); + } + + public void setTaskColor(LocalDateTime taskDateTime) { + LocalDateTime todayDate = LocalDateTime.now(); + if (taskDateTime.isBefore(todayDate)) { + id.getStyleClass().add("cell_big_label_late"); + name.getStyleClass().add("cell_big_label_late"); + date.getStyleClass().add("cell_small_label_late"); + linkLabel.getStyleClass().add("cell_small_label_late"); + } else if (taskDateTime.isBefore(todayDate.plusDays(3))) { + id.getStyleClass().add("cell_big_label_soon"); + name.getStyleClass().add("cell_big_label_soon"); + date.getStyleClass().add("cell_small_label_soon"); + linkLabel.getStyleClass().add("cell_small_label_soon"); + } else { + id.getStyleClass().add("cell_big_label"); + name.getStyleClass().add("cell_big_label"); + date.getStyleClass().add("cell_small_label"); + linkLabel.getStyleClass().add("cell_small_label"); + } + } + + public void setMarkedImage(Task task) { + if (task.isTaskMark()) { + markImage.setImage(new Image(TICK_ICON)); + } else { + markImage.setImage(new Image(UNTICK_ICON)); + } + + } + + public void setLink() { + if (!(task.getLink().isEmpty()) && !(task.getLink().toString().isEmpty())) { + linkLabel.setText("Link:"); + link.setText(task.getLink().toString()); + link.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent event) { + try { + Desktop.getDesktop().browse(new URI(link.getText())); + } catch (URISyntaxException | IOException e) { + link.getStyleClass().add("cell_small_hyperlink_invalid"); + } + } + }); + } + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof TaskCard)) { + return false; + } + + // state check + TaskCard card = (TaskCard) other; + return id.getText().equals(card.id.getText()) + && task.equals(card.task); + } +} 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..8cc91e33d90 --- /dev/null +++ b/src/main/java/seedu/address/ui/TaskListPanel.java @@ -0,0 +1,49 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.task.Task; + +/** + * Panel containing the list of tasks. + */ +public class TaskListPanel extends UiPart { + 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 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/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..bd21bd8b853 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/nuslogo.jpg"; private Logic logic; private MainWindow mainWindow; @@ -48,6 +48,7 @@ public void start(Stage primaryStage) { logger.severe(StringUtil.getDetails(e)); showFatalErrorDialogAndShutdown("Fatal error during initializing", e); } + } private Image getImage(String imagePath) { diff --git a/src/main/java/seedu/address/ui/exceptions/UiException.java b/src/main/java/seedu/address/ui/exceptions/UiException.java new file mode 100644 index 00000000000..7630abc8b24 --- /dev/null +++ b/src/main/java/seedu/address/ui/exceptions/UiException.java @@ -0,0 +1,14 @@ +package seedu.address.ui.exceptions; + +public class UiException extends Exception { + public UiException(String message) { + super(message); + } + + /** + * Constructs a new {@code UiException} with the specified detail {@code message} and {@code cause}. + */ + public UiException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png deleted file mode 100644 index 29810cf1fd9..00000000000 Binary files a/src/main/resources/images/address_book_32.png and /dev/null differ diff --git a/src/main/resources/images/nuslogo.jpg b/src/main/resources/images/nuslogo.jpg new file mode 100644 index 00000000000..e486a3b899b Binary files /dev/null and b/src/main/resources/images/nuslogo.jpg differ diff --git a/src/main/resources/images/tick.png b/src/main/resources/images/tick.png new file mode 100644 index 00000000000..2902d24a880 Binary files /dev/null and b/src/main/resources/images/tick.png differ diff --git a/src/main/resources/images/untick.png b/src/main/resources/images/untick.png new file mode 100644 index 00000000000..6ded20116ec Binary files /dev/null and b/src/main/resources/images/untick.png differ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 09f6d6fe9e4..60b069a1af0 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -4,6 +4,6 @@ - + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..b7fa5ce3b72 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,3 +1,4 @@ + .background { -fx-background-color: derive(#1d1d1d, 20%); background-color: #383838; /* Used in the default.html file */ @@ -5,28 +6,28 @@ .label { -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: #555555; + -fx-font-family: "Roboto"; + -fx-text-fill: white; -fx-opacity: 0.9; } .label-bright { -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; + -fx-font-family: "Roboto"; -fx-text-fill: white; -fx-opacity: 1; } .label-header { -fx-font-size: 32pt; - -fx-font-family: "Segoe UI Light"; + -fx-font-family: "Roboto"; -fx-text-fill: white; -fx-opacity: 1; } .text-field { -fx-font-size: 12pt; - -fx-font-family: "Segoe UI Semibold"; + -fx-font-family: "Roboto"; } .tab-pane { @@ -66,7 +67,7 @@ .table-view .column-header .label { -fx-font-size: 20pt; - -fx-font-family: "Segoe UI Light"; + -fx-font-family: "Roboto"; -fx-text-fill: white; -fx-alignment: center-left; -fx-opacity: 1; @@ -79,12 +80,14 @@ .split-pane:horizontal .split-pane-divider { -fx-background-color: derive(#1d1d1d, 20%); -fx-border-color: transparent transparent transparent #4d4d4d; + -fx-box-border: transparent; } .split-pane { -fx-border-radius: 1; -fx-border-width: 1; -fx-background-color: derive(#1d1d1d, 20%); + -fx-box-border: transparent; } .list-view { @@ -116,40 +119,83 @@ -fx-border-width: 1; } -.list-cell .label { +.cell_big_label { + -fx-font-family: "Roboto"; + -fx-font-size: 16px; -fx-text-fill: white; } -.cell_big_label { - -fx-font-family: "Segoe UI Semibold"; +.cell_big_label_late { + -fx-font-family: "Roboto"; -fx-font-size: 16px; - -fx-text-fill: #010504; + -fx-text-fill: #BC544B; +} + +.cell_big_label_soon { + -fx-font-family: "Roboto"; + -fx-font-size: 16px; + -fx-text-fill: #ccad10; } .cell_small_label { - -fx-font-family: "Segoe UI"; - -fx-font-size: 13px; - -fx-text-fill: #010504; + -fx-font-family: "Roboto"; + -fx-font-size: 12px; + -fx-text-fill: white; +} + +.cell_small_label_late { + -fx-font-family: "Roboto"; + -fx-font-size: 12px; + -fx-text-fill: #BC544B; +} + +.cell_small_label_soon { + -fx-font-family: "Roboto"; + -fx-font-size: 12px; + -fx-text-fill: #ccad10; +} + +.cell_small_hyperlink { + -fx-font-family: "Roboto"; + -fx-font-size: 12px; + -fx-text-fill: #83CDE6; +} + +.cell_small_hyperlink_invalid { + -fx-font-family: "Roboto"; + -fx-font-size: 12px; + -fx-text-fill: #FFCCCC; } .stack-pane { -fx-background-color: derive(#1d1d1d, 20%); } +.pane-with-only-top-border { + -fx-border-style: solid none none none; + -fx-background-color: derive(#1d1d1d, 20%); +} + +.pane-with-no-border { + -fx-border-style: none; + -fx-background-color: derive(#1d1d1d, 20%); +} + .pane-with-border { -fx-background-color: derive(#1d1d1d, 20%); -fx-border-color: derive(#1d1d1d, 10%); - -fx-border-top-width: 1px; } .status-bar { + -fx-font-family: "Roboto"; + -fx-font-size: 5pt; -fx-background-color: derive(#1d1d1d, 30%); } .result-display { -fx-background-color: transparent; - -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; + -fx-font-family: "Roboto"; + -fx-font-size: 10pt; -fx-text-fill: white; } @@ -158,9 +204,9 @@ } .status-bar .label { - -fx-font-family: "Segoe UI Light"; + -fx-font-family: "Roboto"; -fx-text-fill: white; - -fx-padding: 4px; + -fx-padding: 10px; -fx-pref-height: 30px; } @@ -197,8 +243,8 @@ } .menu-bar .label { - -fx-font-size: 14pt; - -fx-font-family: "Segoe UI Light"; + -fx-font-size: 12pt; + -fx-font-family: "Roboto"; -fx-text-fill: white; -fx-opacity: 0.9; } @@ -218,7 +264,7 @@ -fx-border-width: 2; -fx-background-radius: 0; -fx-background-color: #1d1d1d; - -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; + -fx-font-family: "Roboto", Helvetica, Arial, sans-serif; -fx-font-size: 11pt; -fx-text-fill: #d8d8d8; -fx-background-insets: 0 0 0 0, 0, 1, 2; @@ -307,6 +353,14 @@ -fx-padding: 8 1 8 1; } +.list-view { + -fx-background-radius: 20; +} + +.list-cell, .list-cell::focused { + -fx-border-radius: 20; +} + #cardPane { -fx-background-color: transparent; -fx-border-width: 0; @@ -319,15 +373,23 @@ #commandTextField { -fx-background-color: transparent #383838 transparent #383838; + -fx-border-color: #383838 #383838 #D3D3D3 #383838; -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; -fx-border-insets: 0; -fx-border-width: 1; - -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; + -fx-font-family: "Roboto"; + -fx-font-size: 11pt; -fx-text-fill: white; } +.custom-text-field:hover { + -fx-border-color: #383838 #383838 #ffffff #383838; +} + +.custom-text-field:focus { + -fx-border-color: #383838 #383838 #ffffff #383838; +} + #filterField, #personListPanel, #personWebpage { -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); } diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css index 17e8a8722cd..396b250abea 100644 --- a/src/main/resources/view/HelpWindow.css +++ b/src/main/resources/view/HelpWindow.css @@ -2,18 +2,11 @@ -fx-text-fill: white; } -#copyButton { - -fx-background-color: dimgray; -} - -#copyButton:hover { - -fx-background-color: gray; -} - -#copyButton:armed { - -fx-background-color: darkgray; -} - #helpMessageContainer { -fx-background-color: derive(#1d1d1d, 20%); } + +.hyperlink { + -fx-border-color: transparent; + -fx-text-fill: #83CDE6; +} diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index 5dea0adef70..3a2f98a52b0 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -9,6 +9,7 @@ + @@ -21,20 +22,13 @@ - - + + + + - - - diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index a431648f6c0..bd88d53f8e9 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -3,18 +3,19 @@ + - + + - + - + @@ -23,7 +24,7 @@ - + @@ -33,27 +34,38 @@ - + - + - + - + - + - + - - - - + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f08ea32ad55..1813a266874 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -27,10 +27,10 @@