diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..9bdba7ffcb4 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ + +# VSCode files +.vscode/launch.json +.vscode/settings.json diff --git a/README.md b/README.md index 13f5c77403f..39928c18a61 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,21 @@ -[![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/AY2324S2-CS2103-F09-3/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S2-CS2103-F09-3/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. +**TutorRec** is a desktop application for tutors to manage client contacts. +It is +- CLI based +- with a simple GUI +- and super-fast to use. + +### Links +- [Project Website](https://ay2324s2-cs2103-f09-3.github.io/tp/) +- [User Guide](https://ay2324s2-cs2103-f09-3.github.io/tp/UserGuide.html) +- [Developer Guide](https://ay2324s2-cs2103-f09-3.github.io/tp/DeveloperGuide.html) +- [About Us](https://ay2324s2-cs2103-f09-3.github.io/tp/AboutUs.html) + +### Acknowledgements +- 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. diff --git a/build.gradle b/build.gradle index a2951cc709e..41ea15bcd3c 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,11 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'tutorrec.jar' } defaultTasks 'clean', 'test' + +run { + enableAssertions = true +} diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..35280ae55bf 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,51 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Chin Zhe Ning - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/biinnnggggg)] -* Role: Project Advisor +* Roles: Documentation, In charge of `Logic` +* Responsibilities: looks after quality of various project documents, diagrams and the `Logic` component +### Jonathan Chong -### Jane Doe + - +[[github](http://github.com/jonchong98)] -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +* Roles: User Interface design and implementation +* Responsibilities: creating UI mockups and implementing design into product -* Role: Team Lead -* Responsibilities: UI -### Johnny Doe +### Darylgolden - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/darylgolden)] -* Role: Developer -* Responsibilities: Data +* Roles: Git expert, Testing, Integration +* Responsibilities: helps other team member with Git matters, ensures + the testing of the project is timely and done properly, In charge of + versioning of the code, maintaining the code repository, integrating + various parts of the software to create a whole. -### Jean Doe +### Aidan Goh - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/TopKec)] -* Role: Developer -* Responsibilities: Dev Ops + Threading +* Role: Code Quality, In charge of `Model` package +* Responsibilities: looks after code quality, ensures adherence to coding +standards, etc., looks after quality of `Model` package -### James Doe +### Tan Qin Yong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/tanqinyong)] -* Role: Developer -* Responsibilities: UI +* Role: Scheduling and Tracking +* Responsibilities: in charge of defining, assigning and tracking of tasks diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 1b56bb5d31b..c60351a935b 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -5,11 +5,12 @@ title: Developer Guide * Table of Contents {:toc} +
-------------------------------------------------------------------------------------------------------------------- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* Existing AB3 code structure and implementation was referenced for new TutorRec features. -------------------------------------------------------------------------------------------------------------------- @@ -18,6 +19,7 @@ title: Developer Guide Refer to the guide [_Setting up and getting started_](SettingUp.md). -------------------------------------------------------------------------------------------------------------------- +
## **Design** @@ -74,6 +76,10 @@ The **API** of this component is specified in [`Ui.java`](https://github.com/se- 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. +`PersonCard` strictly represents a UI element depicting a `Person` object as found in the `Model` component. + +`ResultDisplay` is used to display any kind of output generated by commands. This includes command execution success/fail messages, command execution results and help messages. Command execution results vary depending on the command executed. + 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, @@ -83,6 +89,8 @@ 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) @@ -114,6 +122,8 @@ 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) @@ -122,17 +132,12 @@ How the parsing works: The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). +* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object) +and all associated `Appointment` objects (which are contained in a `DisjointAppointmentList`). * stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
- - - -
- ### Storage component @@ -155,6 +160,76 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. +### Managing appointments + +#### Implementation + +A core feature of TutorRec is the ability to add appointments to a contact. +Appointments are added with the field (`/ap`) when doing an `add` or `edit` command, for instance: + +- `add n/John Doe /ap 12:00-13:00 MON` will add a person with the name "John Doe" +and an appointment on Monday from 12:00 to 13:00 to the contact list. + +The following diagram describes the process of adding a valid Person appointment with the `add` command: + +![AddSequenceDiagramAppointment](images/AddAppointmentSequenceDiagramMain.png) + +The appointments, after being parsed, are stored in a list of appointments within `Person`. +This is implemented as an `AppointmentList` field. +To prevent appointment overlap, we check both +1. `Appointment` overlap within `Person`'s `AppointmentList` and +2. `Appointment` overlap between the appointments in `AppointmentList` and the existing appointments in `Model`. + +![AddSequenceDiagramRefFrame](images/AddAppointmentSequenceDiagramCheck.png) + + +Note that certain details, such as other fields in a `Person` have been omitted for brevity. + + +**Design Considerations** + +All existing appointments are stored in `Model` in a `DisjointAppointmentList`. We chose to make this distinction between +`AppointmentList` and `DisjointAppointmentList` to allow for easy utility of `AppointmentList` in storing and working with +parsed appointments. + +
+ +### Notes for students +#### Implementation + +TutorRec is able to add notes to each student. They are added as a field (`/nt`) when doing an `add` or `edit` command, so something similar to: + +`edit 1 /nt "This student is very good at math, but struggles with English."` will edit the person on index 1 to have the note "This student is very good at math, but struggles with English." + +### Duplicate contacts +#### Implementation + +In TutorRec, contacts are uniquely identified by their names. No two contacts can have the exact same name, ensuring that duplicate contacts are not created. + +**Case Insensitivity**: Contact names in TutorRec are not case-sensitive. For example, 'John Doe' and 'JOhn dOE' are treated as the same name. + +**Whitespace Sensitivity**: Unlike case sensitivity, whitespaces in names do affect differentiation. Thus, 'Mary Anne' and 'Maryanne' are recognized as distinct names due to the difference in whitespace. + +**Handling Potential Duplicates**: Whenever a user attempts to add or edit a contact, TutorRec checks for names that might be similar by ignoring differences in case or whitespace. If a potential duplicate is detected, the user is warned when the contact is added. + +**Contact Information Flexibility**: Unlike names, a contact's phone number and email address do not have to be unique in TutorRec. This allows for scenarios where a single contact detail, such as a phone number or email, might be associated with multiple contacts, such as a parent with several children enrolled. This design decision facilitates easier management of family-related records, ensuring that it is permissible for different contacts to share identical contact information. + +
+ +### Listing Students + +#### Implementation + +TutorRec is also able to list all current students in the address book. Note that the command `list` does not modify the address book. Additionally, it does not take in any extra parameters. It can be simply called as follows: + +- `list` will show all current students in the address book in the `PersonListPanel` + +The example shown below will describe the process for listing all students during the `list` command. + +![ListSequenceDiagram](images/ListSequenceDiagram.png) + +
+ ### \[Proposed\] Undo/redo feature #### Proposed Implementation @@ -220,6 +295,8 @@ Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Sinc ![UndoRedoState5](images/UndoRedoState5.png) +
+ The following activity diagram summarizes what happens when a user executes a new command: @@ -237,13 +314,6 @@ The following activity diagram summarizes what happens when a user executes a ne * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). * Cons: We must ensure that the implementation of each individual command are correct. -_{more aspects and alternatives to be added}_ - -### \[Proposed\] Data archiving - -_{Explain here how the data archiving feature will be implemented}_ - - -------------------------------------------------------------------------------------------------------------------- ## **Documentation, logging, testing, configuration, dev-ops** @@ -262,13 +332,14 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts +* busy home tutor for primary school students +* has a need to manage a large number of students * 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**: provides easy access to client info and organizes it in an efficient and readable way for day-to-day use, optimized for tutors that prefer CLI. ### User stories @@ -277,51 +348,313 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli | 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}* +| `* *` | new home tutor | have adding information be intuitive and logical | use the app without hassle | +| `* *` | new user | have a quick start guide | learn how to use the app | +| `* *` | new home tutor | follow a set syllabus for my clients | | +| `* *` | new home tutor | be able to quickly create a student’s profile with their relevant information | | +| `* *` | new home tutor | track the performance of my students | prove to their parents that they are improving and my effectiveness as a tutor | +| `* * *` | new home tutor | track all my appointments | reduce the chance I forget or double book myself | +| `* *` | new home tutor | keep track of my finances–such as which client has paid me for my work over a month | reduce my stress when it comes to keeping track of who has paid me for my work, and so forth. | +| `* *` | busy home tutor | seamlessly create new tasks | quickly and efficiently set up my daily routine | +| `* *` | busy home tutor | be reminded about my upcoming appointments for the day | not accidentally forget | +| `* *` | returning home tutor | re-implementation of more information in a large amount to be easy | add information en masse without stress | +| `* *` | home tutor with clients who are nearing exam season | properly ensure that they are improving as planned, and also allow them to set benchmarks that I can remember | | +| `* *` | busy home tutor | block certain times out for lunch and dinner | not accidentally overwrite those times with an additional client | +| `* * *` | experienced home tutor | delete students I am no longer teaching on TutorRec | unclutter my interface | +| `* * *` | experienced home tutor | update student information and details | ensure accurate records are maintained | +| `* * *` | passionate home tutor | TutorRec to keep notes for each student | tailor my teaching style accordingly | +| `* *` | experienced home tutor with many students | ability to categorise students by skill level, subject or group (p1, p2, p3…) | quickly locate their information when needed | +| `* *` | experienced home tutor | flexibility to choose what to teach my students (custom lessons) | personalise each lesson for different students | +| `* *` | experienced home tutor | easily identify students with weak performance | focus on weaker students | +| `* *` | experienced home tutor | view a student’s records for the length of time I have tutored them | | +| `* *` | experienced home tutor | get some insights and analytics on a student’s performance over time | further refine my teaching methods accordingly | +| `* *` | busy home tutor | quickly reschedule my appointments with my clients | fit my ever-changing schedule, preventing a large amount of hassle | +| `* *` | tutor who just received a new wave of clients | separate my old and new clients | keep the interface orderly and easy-to-follow | +| `* * *` | busy home tutor | make quick notes about my students | keep track of information specific to each client | +| `* *` | assignment-ridden home tutor | note cancellations in my schedule due to increased workload from my end | my schedule is accurate to reality | +| `* *` | wary home tutor | backup and import data | safeguard myself against potential data corruption and/or physical destruction of my devices | +| `* *` | home tutor with a new device | quickly transfer data from one device to another | prevent the pain of having to input previous data manually | +| `* *` | online tutor | organize sessions with students in a different country and automatically convert dates and times to my time zone | | +| `* *` | online home tutor | keep Zoom links with sessions and other information about the student in one place | | +| `* *` | online home tutor with students who are abroad | convert time at a glance | be on time for my student’s lessons | +| `* * *` | home tutor who just moved abroad | remove previous clients I cannot tutor due to the distance gap | keep my schedule clean | +| `* *` | home tutor who just moved abroad | ways of tagging my students | keep track of different needs arising due to cultural differences | + +
### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is the `TutorRec` and the **Actor** is the `user`, unless specified otherwise) + +**Use case: Delete a student** + +**MSS** + +1. User requests to list students +2. TutorRec shows a list of students +3. User requests to delete a specific student in the list +4. TutorRec deletes the student + + Use case ends. + +**Extensions** + +* 2a. The list is empty. + + Use case ends. + +* 3a. The given index is invalid. + + * 3a1. TutorRec shows an error message. + + Use case resumes at step 2. -**Use case: Delete a person** + +**Use case: Viewing appointments** **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 view current appointments for the day +2. TutorRec shows a list of appointments for the day Use case ends. **Extensions** +* 1a. The list is empty. + + Use case ends. + +* 1b. User inputs an invalid day format as an input. + + * 1b1. TutorRec shows an error message. + + Use case ends. + + +**Use case: Sorting students** + +**MSS** + +1. User requests to view students of a particular category +2. TutorRec shows a filtered list containing only students with this category + + Use case ends. + +**Extensions** + +* 1a. The list is empty. + + Use case ends. + +* 1b. The given category does not have any students assigned to it. + + * 1b1. TutorRec shows an empty list. + + Use case ends. + +* 1c. User inputs an invalid category. + + * 1c1. TutorRec shows an error message. + + Use case ends. + + +**Use case: Editing a student's details** + +**MSS** + +1. User requests to list students +2. TutorRec displays a list of students +3. User requests to edit the details of a specific student in the list +4. TutorRec updates the details of this student +5. TutorRec displays the updated information of this student + + Use case ends. + +**Extensions** + * 2a. The list is empty. Use case ends. * 3a. The given index is invalid. - * 3a1. AddressBook shows an error message. + * 3a1. TutorRec shows an error message. Use case resumes at step 2. -*{More to be added}* +* 3b. User inputs a field that does not exist (e.g. adding a nonexistent /q field). + + * 3b1. TutorRec shows an error message. + + Use case resumes at step 2. + + +**Use case: Finding a student** + +**MSS** + +1. User requests to list all students with a particular name +2. TutorRec displays a reduced list containing all students that meet the criteria of the name requested + + Use case ends. + +**Extensions** + +* 1a. The list is empty. + + Use case ends. + +* 1b. No students exist with the given name. + + * 1b1. TutorRec displays an empty list. + + Use case ends. + +**Use case: Checking improvements of a student** + +**MSS** + +1. User requests to list students +2. TutorRec displays a list of students +3. User updates a specific student's grades for a given test +4. TutorRec updates the grades for this student +5. TutorRec displays that the student's grades has been updated +6. User requests to view a list of a specific student's grades +7. TutorRec displays a history of this student's grades + + Use case ends. + +**Extensions** + +* 2a. The list is empty. + + Use case ends. + +* 3a. The given index is invalid. + + * 3a1. TutorRec shows an error message. + + Use case resumes at step 2. + +* 3b. An invalid score is listed as the input. + + * 3b1. TutorRec shows an error message. + + Use case resumes at step 2. + +* 6a. The given index is invalid. + + * 6a1. TutorRec shows an error message. + + Use case resumes at step 2. + +* 6b. The student has no grades saved. + + * 6b1. TutorRec displays nothing. + + Use case resumes at step 2. + +**Use case: Updating payment status** + +**MSS** + +1. User requests to list students +2. TutorRec displays a list of students +3. User chooses to mark a specific student as having made their payment +4. TutorRec updates the payment status of this student to be complete +5. User chooses to mark a specific as not having made their payment +6. TutorRec updates the payment status of this student to be incomplete + + Use case ends. + +**Extensions** + +* 2a. The list is empty. + + Use case ends. + +* 3a. The given index is invalid. + + * 3a1. TutorRec shows an error message. + + Use case resumes at step 2. + +* 3b. The student selected already has had their payment marked as made. + + * 3b1. TutorRec shows an error message. + + Use case resumes at step 2. + +* 5a. The given index is invalid. + + * 5a1. TutorRec shows an error message. + + Use case resumes at step 2. + +* 5b. The student selected already has had their payment marked as made. + + * 5b1. TutorRec shows an error message. + + Use case resumes at step 2. + + +**Use case: Creating an appointment** + +**MSS** + +1. User requests to list students +2. TutorRec displays a list of students +3. User sets a specific student to have an appointment at a particular time and date +4. TutorRec updates details about this student +5. TutorRec displays details of appointment to user + + End of use case. + +**Extensions** + +* 2a. The list is empty. + + Use case ends. + +* 3a. The given index is invalid. + + * 3a1. TutorRec shows an error message. + + Use case resumes at step 2. + +* 3b. Insufficient information is given to make an appointment. + + * 3b1. TutorRec shows an error message. + + Use case resumes at step 2. + +* 3c. The time and date inputted by the user clashes with an existing appointment previously made by the user. + + * 3c1. TutorRec shows an error message. + * 3c2. TutorRec displays information of student which has an appointment that resulted in the timing clash, and the date and time of this appointment. + + Use case resumes at step 2. + +
### Non-Functional Requirements -1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +1. TutorRec should work on any _mainstream OS_ as long as it has Java `11` or above installed. +2. TutorRec 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. - -*{More to be added}* +4. TutorRec is not required to handle multiple users (i.e. multi-user product); it is a single-user product. +5. TutorRec needs to be developed in a breadth-first incremental manner, with weekly updates. +6. TutorRec's data should be stored locally and should be in a human editable text file. +7. TutorRec should not use a DBMS to store data. +8. TutorRec should work without requiring an installer. +9. TutorRec should be packaged into a single JAR file for releases. +10. TutorRec's JAR files should not exceed 100MB. +11. The development of TutorRec should follow the Object-oriented paradigm. +12. TutorRec should be able to respond within three seconds. +13. TutorRec should be designed to function offline. +14. TutorRec should not require additional hardware beyond standard computing devices (e.g., desktops, laptops, tablets) commonly availabe to users. ### Glossary @@ -329,6 +662,7 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli * **Private contact detail**: A contact detail that is not meant to be shared with others -------------------------------------------------------------------------------------------------------------------- +
## **Appendix: Instructions for manual testing** @@ -345,16 +679,19 @@ testers are expected to do more *exploratory* testing. 1. Download the jar file and copy into an empty folder - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 1. Open a terminal inside the folder containing the jar file and run `java -jar tutorrec.jar`
Expected: Shows the GUI with a set of sample persons. The window size may not be optimum. 1. Saving window preferences 1. Resize the window to an optimum size. Move the window to a different location. Close the window. - 1. Re-launch the app by double-clicking the jar file.
+ 1. Re-launch the app by running `java -jar tutorrec.jar` in the folder containing the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ +1. Shutting down + + 1. Use the `exit` command or click on the close window button on the title bar + of TutorRec. ### Deleting a person @@ -371,12 +708,49 @@ testers are expected to do more *exploratory* testing. 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 …​ }_ +### Viewing all appointments / appointments on a specific day + +1. Viewing all appointments + 1. Prerequisites: TutorRec is running and contains persons with appointments. Each day of the week has at least 1 appointment belonging to any person. The current displayed list of persons is displaying all persons. + 2. Test case: `appointments` +
Expected: All appointments of the persons currently displayed in the list are displayed. +2. Viewing appointments on a specific day + 1. Prerequisites: TutorRec is running and contains persons with appointments. There is at least one appointment scheduled on Monday belonging to any person in the current displayed list. + 2. Test case: `appointments MON` +
Expected: Appointments scheduled on Monday of the persons current displayed in the list are displayed. + +### Viewing details of a person + +1. Viewing details of a person currently displayed in the list + 1. Prerequisites: There is at least one person in the current displayed list. + 2. Run `view 1`. +
Expected: All details of the person are displayed in the window on the right. ### Saving data -1. Dealing with missing/corrupted data files +1. Dealing with missing data file + + 1. Open a terminal and run `java -jar tutorrec.jar` in the folder containing the jar file. This will cause TutorRec to generate a data folder containing addressbook.json which stores all person data. + 2. Exit TutorRec. + 3. Delete the addressbook.json file in the data folder. + 4. Run `java -jar tutorrec.jar` in the terminal again.
Expected: TutorRec regenerates the data file containing some sample persons. - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ +-------------------------------------------------------------------------------------------------------------------- -1. _{ more test cases …​ }_ +## **Appendix: Planned Enhancements** + +Team Size: 5 + +1. **Standardize display of person's contact details with other fields when missing information:** Currently when a person is missing contact information such as phone number, email and address, the view command shows '---' instead of just '-' like other fields when there's no information. This will be changed in a future version to show the same '-' as other fields when information is missing. +2. **Improve UI to handle extremely long tags going out of UI bounds:** Currently when a tag contains a string that is extremely long, it will stretch beyond the boundary of the person list. This will be improved in a future version to allow the tag to wrap around to display the full tag. +3. **Set a limit to the length of a name:** Currently TutorRec is capable of accepting a name of any length. This causes issues when the name entered is excessively long, resulting in it being not displayed properly in the UI. Setting a limit on the length of a name will prevent it from being cut-off in the UI. +4. **Fix the `find` command to be reflective of the current list:** Currently, the `find` command properly filters the user list when the command is typed, but does not respond to `delete` commands properly. For example, if a person "David" is searched for in the list, and `find David` is used, then it currently will properly display a filtered list with "David" in it. Suppose David has index 1. + - If `delete 1` is used, the filtered list will incorrectly still list David as still being in the list of contacts. + - If `edit 1 [some edits]` is used, the filtered list will incorrectly completely reset to show the entire full list, with or without David. + - In either case, any command to modify the list should properly interact with the previous `find` command, so it should remove David as part of the contact list if `delete` is used, or continue showing the filtered list if `edit` is used, and so forth. +5. **Improve error message handling:** Currently, there are certain user-inputted errors which are not properly reflected in TutorRec. For example, typing `edit 1 n/` is properly identified as an invalid command (as a person **must not** have their name removed), but the feedback given by TutorRec is that names should only be alphanumeric, and should not be blank - which is technically correct, but does not communicate sufficiently with the user that the `Name` field cannot be removed in `edit`. +6. **Improve appointment conflict detection:** Currently, TutorRec disallows conflicting appointments, that is to say, appointments that overlap at a particular time on a particular day. This however, does not account for special cases where the timing of an appointment is different for two days but on the same day, (For example, two appointments, one on the current Sunday, and one on the next Sunday, the former from 1400-1700 and the latter at 1500-1800 is marked as a conflict.) and so an improvement to TutorRec's way of detecting conflicting appointments could improve user experience. +7. **Allow special characters as part of names:** Currently, TutorRec strictly allows **only** alphanumeric characters to be part of a name. However, this does not account for people with special characters as part of their legal names, such as the use of `s/o` and `-`. This improvement would allow for accurate recording of names. +8. **Improve consistency of multiple prefixes:** Currently, TutorRec accepts multiple arguments for certain prefixes. For example, users can have multiple prefixes for `subject`, `edit 4 s/ENGLISH s/MATH` resulting in the student having both subjects. However, for the `note` and `level` prefixes, TutorRec allows multiple prefixes but only takes the last argument. `edit 4 l/p1 l/p6` results in the student having only the `p6` level assigned. To improve on this, we can enhance the consistency of accepting multiple prefixes across all commands in the project. +9. **Enable resizing of TutorRec:** Currently, TutorRec is not resizable as a window. This change would improve user experience. +10. **Improve duplicate name detection in TutorRec:** Presently, TutorRec permits additional white spaces between names, such as **'John     Doe'**, which is distinguished as a separate name from **'John Doe'**. We can refine duplicate name detection by devising a more robust algorithm to address this issue and efficiently identify near matches. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 7abd1984218..92821758284 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,7 +3,7 @@ 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. +TutorRec is a **desktop app for 1-to-1 primary school home tutors to manage student 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, TutorRec can get your contact management tasks done faster than traditional GUI apps. * Table of Contents {:toc} @@ -14,20 +14,20 @@ 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 `tutorrec.jar` from [here](https://github.com/AY2324S2-CS2103-F09-3/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 _home folder_ for TutorRec. -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
+1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar tutorrec.jar` command to run the application.
+ A GUI similar to the one 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 a command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
Some example commands you can try: * `list` : Lists all contacts. - * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. + * `add n/Jun Jie p/98765432 e/jj@example.com a/Clementi Ave 3, block 442, #06-01 nt/Weak at Maths t/referral ap/10:00-12:00 SAT s/MATH l/P1` : Adds a contact named `Jun Jie` to the address book. * `delete 3` : Deletes the 3rd contact shown in the current list. @@ -38,6 +38,7 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo 1. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- +
## Features @@ -46,13 +47,13 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo **:information_source: Notes about the command format:**
* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. + e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/Jun Jie`. * Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. + e.g `n/NAME [t/TAG]` can be used as `n/Jun Jie t/` or as `n/Jun Jie`. * Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. + e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/Raffles`, `t/Raffles t/ALevel` etc. * Parameters can be in any order.
e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. @@ -60,12 +61,95 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo * Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. +* Prefixes are adjusted to accept predefined convenient short forms, + e.g., `hp/`, `addr/`, `subj/`, `lvl/`. + + * You may choose to use short form or long form of prefixes, e.g., `n/` or `name/`, which are interchangeable. + + * Prefixes are also adjusted to accept some predefined slightly incorrect variations, in case of user typos. + The full list of accepted typos and short forms are listed below: + * `n/`: `name/` `nae/` `nam/` + * `p/`: `phone/` `phon/` `hp/` `handphone/` + * `e/`: `email/` `emai/` `em/` `ema/` + * `a/`: `address/` `addr/` `add/` `ad/` `addres/` `adress/` + * `p/`: `phone/` `phon/` `hp/` `handphone/` + * `nt/`: `note/` `not/` `nt/` + * `t/`: `tag/` `ta/` `tg/` + * `ap/`: `appointment/` `appt/` `appoint/` `appointmen/` + * `s/`: `subject/` `subj/` `sub/` `subjec/` `subje/` + * `l/`: `level/` `lvl/` `leve/` `lv/` `lev/` `lvel/` `evel/` + +* TutorRec is currently **not** resizable + * If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. +
+ +### Formatting fields for a person + +A person has the following fields: `Name`, `Phone`, `Email`, `Address`, `Note`, `Tag`, `Appointment`, +`Subject` and `Level`. + +Below lists the requirements for each to be a valid field. + +- `Name`: Must be alphanumeric. + - `Jane`, `Jane1` are valid + - `Jane@`, `**&&&` are not valid. +- `Phone`: Must only contain numbers + - `999`, `12341234` are valid. + - `123Phone`, `aeiou` are not valid. +- `Email`: Contains two parts, in the format `local-part@domain` + - `local-part` must adhere to the following restrictions: + - Contain only alphanumeric characters + - May contain the following special characters `+_.-` + - May not begin with the above mentioned special characters + - `domain` must adhere to the following restrictions: + - contain only letters, numbers, and dashes (`-`), note that hyphens **cannot** be the first or last characters of the domain + - the final part of the domain: + - is defined by a `.` to separate it from other parts of the domain + - is defined by the entire domain if no `.` is present + - must be at least two characters long + - ergo, the following are examples of domains which are valid and invalid: + - `cc`, `test.com`, `name-separator.gov` are valid + - `a`, `t*.ab`, `invalid.sep-` are not valid + - With these restrictions in mind, the following are some valid and invalid emails: + - `alex@example.com`, `jorge@website.site.com`, `jack_jane.john@example.com` are valid. + - `alex@@example.com`, `jorge@website.site.com-`, `jack&jane*john@example.com` are not valid. +- `Address`: Must not be blank or contain only spaces. + - Note that entering an address which is blank or has spaces will instead treat a person as having no address. Valid addresses are still only those which do not violate the above criteria. +- `Note`: Must not be blank or contain only spaces + - See above. +- `Tag`: Should be alphanumeric: [a-zA-Z0-9] +- `Subject`: Must be `MATH`, `SCIENCE`, `ENGLISH` or `MT`. +- `Level`: Must be `P1`, `P2`, `P3`, `P4`, `P5` or `P6`. +- `Appointment`: Must be in the format `START_TIME-END_TIME DAY` + - `START_TIME` and `END_TIME` are in the 24-hour format of `HH:MM`. The time of `START_TIME` must strictly be smaller than `END_TIME` + - `DAY` must be one of the following: `MON`, `TUE`, `WED`, `THU`, `FRI`, `SAT`,`SUN`. + - Respectively, these represent Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, and Sunday. + - These are not case-sensitive + - `12:00-13:00 MON`, `16:59-22:00 sun` are valid. + - `13:00-11:00 MON`, `16:0000-19:1234 MON`, `16:00-17:00 SUNDAY` are not valid. + - Overlapping appointments between students are strictly not allowed as TutorRec is for tutors who provide 1-to-1 tutoring. + +### Duplicate detection for names and contacts + +* TutorRec does not allow for duplicate contacts, and contacts are differentiated by their unique names. + + * Names are not case-sensitive, `John Doe` is the same name as `JOhn dOE` + + * Whitespaces do differentiate names apart, e.g., `Mary Anne` is a different name (and person) from `Maryanne`. + +* TutorRec's duplicate detection system ignores case and extra whitespace when comparing names. + + * When adding or editing a contact, if a similar name is detected, regardless of case or whitespace differences, users + are warned about potential duplicates. + +* In TutorRec, a contact’s phone number and email address do not need to be unique. This flexibility allows you to save the same contact details for parents who have multiple children enrolled with you. As such, it is acceptable for different contacts to share the same phone number and email address. + ### Viewing help : `help` -Shows a message explaning how to access the help page. +Shows a message explaining how to access the help page. ![help message](images/helpMessage.png) @@ -76,15 +160,37 @@ Format: `help` Adds a person to the address book. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +Format: `add n/NAME [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [nt/NOTE] [ap/APPOINTMENT]… [t/TAG]… [s/SUBJECT]… [l/LEVEL]​`
:bulb: **Tip:** -A person can have any number of tags (including 0) +A person can have any number of tags, and any number of appointments (including 0).
Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* `add n/Jun Jie p/98765432 e/jj@example.com a/Clementi Ave 3, block 442, #06-01 s/MATH` +* `add n/Monica Chng e/mc@example.com a/Dempsey Hill p/81888818 ap/10:00-12:00 FRI l/P6` +* `add n/Abel nt/exstudent ap/12:00-15:00 SUN ap/18:00-22:00 TUE` + +Only the "Name" field is mandatory. If you do not wish to have the other fields to have values, you can add the person +in without the corresponding prefix, or leaving the prefix blank. + +For example: +* `add n/John` +* `add n/John a/` + +Both create the same person in the address book (i.e. a person named "John" with no address). +The same logic applies to the other fields. + + +
:warning: **Note:** +New appointments **must not overlap with each other** and should not overlap with existing appointments. +
+ +
:exclamation: **Caution:** +When it comes to notes and levels, TutorRec will only consider the final prefix in cases where there are multiple prefixes. +
+ +For example, with the command: `add n/John l/p1 l/p2`, TutorRec will only consider `l/p2`. ### Listing all persons : `list` @@ -92,39 +198,59 @@ Shows a list of all persons in the address book. Format: `list` +### Viewing a person : `view` + +Displays all information of a person on the side window. + +Format: `view INDEX` + +* Displays all information of 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, ... + ### Editing a person : `edit` Edits an existing person in the address book. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Format: `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [nt/NOTE] [ap/APPOINTMENT]…​ [t/TAG]…​ [s/SUBJECT]…​ [l/LEVEL]` -* 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 person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. +The index **must be a positive integer** 1, 2, 3, …​ * At least one of the optional fields must be provided. * Existing values will be updated to the input values. * When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. * You can remove all the person’s tags by typing `t/` without - specifying any tags after it. + specifying any tags after it. Appointments work similarly (i.e. typing `ap/` with no appointments after it clears all appointments) +* You can remove fields (except for name) by typing the prefix for the relevant field and leaving it blank. +* Note that while the edit command format specifies that the fields `nt/` and `l/` accepts one argument only, + TutorRec v1.4 still accept multiple inputs, however, only the last input will be processed. 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. +* `edit 1 p/91234567 e/jj@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `jj@example.com` respectively. +* `edit 2 n/Monica Chng t/` Edits the name of the 2nd person to be `Monica Chng` and clears all existing tags. +* `edit 3 n/Bobby Brown p/` Edits the name of the 3rd person to be `Bobby Brown` and removes the `phone` field. + +
:exclamation: **Caution:** +When it comes to notes and levels, TutorRec will only consider the final prefix in cases where there are multiple prefixes. +
-### Locating persons by name: `find` +For example, with the command: `edit 1 l/p1 l/p2`, TutorRec will only consider `l/p2`. + +### Locating persons by name : `find` Finds persons whose names contain any of the given keywords. Format: `find KEYWORD [MORE_KEYWORDS]` -* The search is case-insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` +* The search is case-insensitive. e.g `hans` will match `Hans`. +* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans`. * Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` +* 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 Bo` will return `Hans Gruber`, `Bo Yang`. Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
+* `find Jun` returns `jun` and `Jun Jie`. +* `find alex david` returns `Alex Yeoh`, `David Li`.
![result for 'find alex david'](images/findAlexDavidResult.png) ### Deleting a person : `delete` @@ -139,7 +265,22 @@ Format: `delete INDEX` 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. +* `find Monica` followed by `delete 1` deletes the 1st person in the results of the `find` command. + +### Viewing all appointments : `appointments` + +Displays all appointments of persons currently displayed in the list, sorted, along with the persons involved. +Optionally, you may specify a `DAY` or multiple `DAY`s to further restrict the appointments displayed. + +Format: `appointments [DAY]` + +* `DAY` must be one of `MON`, `TUE`, ..., `SUN`. +* `[DAY]` may be empty. + +Examples: +* `appointments` returns all appointments among the displayed persons. +* `appointments MON` returns all appointments among the displayed persons on Monday. +* `appointments MON TUE` returns all appointments among the displayed persons on Monday and Tuesday. ### Clearing all entries : `clear` @@ -155,15 +296,15 @@ 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. +TutorRec data is saved in the hard disk automatically after any command that changes the data. There is no need to save manually. ### Editing the data file -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +TutorRec data is saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file.
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
-Furthermore, certain edits can cause the AddressBook to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly. +If your changes to the data file makes its format invalid, TutorRec will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
+Furthermore, certain edits can cause TutorRec to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly.
### Archiving data files `[coming in v2.0]` @@ -175,7 +316,7 @@ _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 data file it creates with the file that contains the data in your previous TutorRec home folder. -------------------------------------------------------------------------------------------------------------------- @@ -189,10 +330,13 @@ _Details coming soon ..._ 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` +**Add** | `add n/NAME [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [nt/NOTE] [ap/APPOINTMENT]… [t/TAG]… [s/SUBJECT]… [l/LEVEL]​`
e.g., `add n/Jun Jie p/98765432 e/jj@example.com a/Clementi Ave 3, block 442, #06-01 s/MATH` +**View** | `appointments` **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` +**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [nt/NOTE] [ap/APPOINTMENT]…​ [t/TAG]…​ [s/SUBJECT]…​ [l/LEVEL]`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` **Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` **List** | `list` +**View person details** | `view INDEX` **Help** | `help` +**Exit** | `exit` diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..a7610eca789 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "TutorRec" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2324S2-CS2103-F09-3/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..09d966ad93a 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -288,7 +288,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "TutorRec"; font-size: 32px; } } diff --git a/docs/diagrams/AddAppointmentSequenceDiagramCheck.puml b/docs/diagrams/AddAppointmentSequenceDiagramCheck.puml new file mode 100644 index 00000000000..e9ede3a38c5 --- /dev/null +++ b/docs/diagrams/AddAppointmentSequenceDiagramCheck.puml @@ -0,0 +1,40 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":AddCommand" as AddCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "toAdd:Person" as Person MODEL_COLOR +participant "model:Model" as Model MODEL_COLOR +participant "aps:AppointmentList" as AppointmentList MODEL_COLOR +end box + +[-> AddCommand : checkAppointmentOverlap(model) +activate AddCommand + +AddCommand -> Person : getAppointments() +activate Person + +Person -> AddCommand : aps +deactivate Person + +AddCommand -> Model : appointmentsOverlap(aps) +activate Model + +Model -> AddCommand : false +deactivate Model + +AddCommand -> AppointmentList : isOverlapping() +activate AppointmentList + +AppointmentList -> AddCommand : false +deactivate + +[<- AddCommand + +destroy AddCommand + +@enduml diff --git a/docs/diagrams/AddAppointmentSequenceDiagramMain.puml b/docs/diagrams/AddAppointmentSequenceDiagramMain.puml new file mode 100644 index 00000000000..9319beda2ca --- /dev/null +++ b/docs/diagrams/AddAppointmentSequenceDiagramMain.puml @@ -0,0 +1,42 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":AddCommand" as AddCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "model:Model" as Model MODEL_COLOR +end box + +[-> AddCommand : execute(model) +activate AddCommand + +AddCommand -> Model : hasPerson(toAdd) +activate Model + +Model --> AddCommand : +deactivate Model + + +AddCommand -> AddCommand : checkAppointmentOverlap(model) +activate AddCommand + + +AddCommand -> AddCommand +deactivate AddCommand + + +AddCommand -> Model : addPerson(toAdd) +activate Model + +Model --> AddCommand +deactivate Model + +[<- AddCommand : result +||| + +destroy AddCommand + +@enduml diff --git a/docs/diagrams/AddSequenceDiagramRefFrame.puml b/docs/diagrams/AddSequenceDiagramRefFrame.puml new file mode 100644 index 00000000000..aa893285472 --- /dev/null +++ b/docs/diagrams/AddSequenceDiagramRefFrame.puml @@ -0,0 +1,41 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":ParserUtil" as ParserUtil MODEL_COLOR +participant "ApptSet: Set" as apptSet #Green +participant "a:Appointment" as Appointment #Green +end box + + +activate Model + +Model -> ParserUtil : parseAppointments(appointments) +activate ParserUtil +ParserUtil -> ParserUtil : requireNonNull(appointments) +create apptSet +ParserUtil -> apptSet : new +activate apptSet + +loop Each String in appointments + create Appointment + apptSet -> Appointment : Appointment(a) + Activate Appointment + Appointment -> Appointment : checkArgument(appointment) + Appointment -> apptSet : add(a) + deactivate Appointment +end + +apptSet -> ParserUtil : apptSet +deactivate apptSet + +ParserUtil -> Model : apptSet + +deactivate ParserUtil + +deactivate Model + +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..c74cd2518cd 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -4,18 +4,23 @@ skinparam arrowThickness 1.1 skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR -AddressBook *-right-> "1" UniquePersonList -AddressBook *-right-> "1" UniqueTagList -UniqueTagList -[hidden]down- UniquePersonList -UniqueTagList -[hidden]down- UniquePersonList +AddressBook *-down-> "1" UniquePersonList +AddressBook *-down-> "1" UniqueTagList +AddressBook *-down-> "1" UniqueAppointmentList +UniquePersonList -[hidden]right- UniqueAppointmentList +UniquePersonList -[hidden]left- UniqueTagList +Tag -[hidden]right- Appointment -UniqueTagList -right-> "*" Tag -UniquePersonList -right-> Person +UniqueTagList -down-> "*" Tag +UniqueAppointmentList -down-> "*" Appointment +UniquePersonList -down-> Person Person -up-> "*" Tag +Person -up-> "*" Appointment Person *--> Name Person *--> Phone Person *--> Email Person *--> Address +Person *--> "*" Note @enduml diff --git a/docs/diagrams/ListSequenceDiagram.puml b/docs/diagrams/ListSequenceDiagram.puml new file mode 100644 index 00000000000..5141f83d19a --- /dev/null +++ b/docs/diagrams/ListSequenceDiagram.puml @@ -0,0 +1,47 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant "l:ListCommand" as ListCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute(list) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(list) +activate AddressBookParser + +create ListCommand +AddressBookParser -> ListCommand +activate ListCommand + +ListCommand --> AddressBookParser +deactivate ListCommand + +AddressBookParser --> LogicManager : l +deactivate AddressBookParser + +LogicManager -> ListCommand : execute() +activate ListCommand + +ListCommand -> Model : updateFilteredPersonList() +activate Model + + +deactivate Model + +ListCommand --> LogicManager : result +deactivate ListCommand +ListCommand -[hidden]-> LogicManager : result +destroy ListCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..20469001469 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -13,12 +13,18 @@ Class ModelManager Class UserPrefs Class UniquePersonList +Class DisjointAppointmentList +Class AppointmentList Class Person Class Address Class Email Class Name Class Phone Class Tag +Class Appointment +Class Note +class Subject +class Level Class I #FFFFFF } @@ -35,20 +41,32 @@ ModelManager -left-> "1" AddressBook ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList +AddressBook *-down-> "1" DisjointAppointmentList +DisjointAppointmentList -[hidden]r-> UniquePersonList +AddressBook *-down-> "1" UniquePersonList UniquePersonList --> "~* all" Person Person *--> Name Person *--> Phone Person *--> Email Person *--> Address Person *--> "*" Tag +Person *--> Note +Person *--> Level +Person *--> "0...4" Subject Person -[hidden]up--> I UniquePersonList -[hidden]right-> I +Person *-left-> "1" AppointmentList +AppointmentList -up-> "*" Appointment +DisjointAppointmentList --> "~* all" Appointment + Name -[hidden]right-> Phone Phone -[hidden]right-> Address Address -[hidden]right-> Email +Email -[hidden]right-> Tag +Tag -[hidden]right-> Subject +Subject -[hidden]right-> Level ModelManager --> "~* filtered" Person @enduml diff --git a/docs/diagrams/add-remark/RemarkClass.puml b/docs/diagrams/NoteClass.puml similarity index 67% rename from docs/diagrams/add-remark/RemarkClass.puml rename to docs/diagrams/NoteClass.puml index 019c1ecbbf1..c281d095cdc 100644 --- a/docs/diagrams/add-remark/RemarkClass.puml +++ b/docs/diagrams/NoteClass.puml @@ -5,15 +5,14 @@ skinparam classAttributeIconSize 0 Class "{abstract}\nCommand" as Command { +execute(Model): CommandResult } -Class RemarkCommand { +Class NoteCommand { +COMMAND_WORD: String +MESSAGE_USAGE: String - +MESSAGE_NOT_IMPLEMENTED_YET: String +execute(Model): CommandResult } Class CommandException -RemarkCommand -up-|> Command +NoteCommand -up-|> Command Command ..> CommandException: throws > -RemarkCommand .right.> CommandException: throws > +NoteCommand .right.> CommandException: throws > @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..02c16e9dbfb 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -1,7 +1,7 @@ @startuml !include style.puml -skinparam arrowThickness 1.1 -skinparam arrowColor UI_COLOR_T4 +skinparam arrowThickness 1.0 +skinparam arrowColor UI_COLOR_T3 skinparam classBackgroundColor UI_COLOR package UI <>{ diff --git a/docs/images/AddAppointmentSequenceDiagramCheck.png b/docs/images/AddAppointmentSequenceDiagramCheck.png new file mode 100644 index 00000000000..1dceb5ef497 Binary files /dev/null and b/docs/images/AddAppointmentSequenceDiagramCheck.png differ diff --git a/docs/images/AddAppointmentSequenceDiagramMain.png b/docs/images/AddAppointmentSequenceDiagramMain.png new file mode 100644 index 00000000000..0ab37f59d4c Binary files /dev/null and b/docs/images/AddAppointmentSequenceDiagramMain.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..03f9414c089 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/ListSequenceDiagram.png b/docs/images/ListSequenceDiagram.png new file mode 100644 index 00000000000..f11a400c3fb Binary files /dev/null and b/docs/images/ListSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..8c327279236 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/NoteClass.png b/docs/images/NoteClass.png new file mode 100644 index 00000000000..07bbaf1dea8 Binary files /dev/null and b/docs/images/NoteClass.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..0698cd09740 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 11f06d68671..781d81ec3f2 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/biinnnggggg.png b/docs/images/biinnnggggg.png new file mode 100644 index 00000000000..00801fcaa0b Binary files /dev/null and b/docs/images/biinnnggggg.png differ diff --git a/docs/images/johndoe.png b/docs/images/darylgolden.png similarity index 100% rename from docs/images/johndoe.png rename to docs/images/darylgolden.png diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png index 235da1c273e..ed20cdae62b 100644 Binary files a/docs/images/findAlexDavidResult.png and b/docs/images/findAlexDavidResult.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..6f1e02689d5 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/jonchong98.png b/docs/images/jonchong98.png new file mode 100644 index 00000000000..875b3d1c11a Binary files /dev/null and b/docs/images/jonchong98.png differ diff --git a/docs/images/tanqinyong.png b/docs/images/tanqinyong.png new file mode 100644 index 00000000000..d7540ed1cbe Binary files /dev/null and b/docs/images/tanqinyong.png differ diff --git a/docs/images/topkec.png b/docs/images/topkec.png new file mode 100644 index 00000000000..11a4a0e692c Binary files /dev/null and b/docs/images/topkec.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..776a3ab53b4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,17 @@ --- layout: page -title: AddressBook Level-3 +title: TutorRec --- -[![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/AY2324S2-CS2103-F09-3/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S2-CS2103-F09-3/tp/actions) +[![codecov](https://codecov.io/gh/AY2324S2-CS2103-F09-3/tp/branch/master/graph/badge.svg)](https://codecov.io/gh/AY2324S2-CS2103-F09-3/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). +**TutorRec 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. +* If you are interested in using TutorRec, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing TutorRec, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** 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/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..97240a565d5 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -36,7 +36,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(0, 3, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/seedu/address/commons/core/GuiSettings.java index a97a86ee8d7..56ffe34340e 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/seedu/address/commons/core/GuiSettings.java @@ -12,8 +12,8 @@ */ public class GuiSettings implements Serializable { - private static final double DEFAULT_HEIGHT = 600; - private static final double DEFAULT_WIDTH = 740; + private static final double DEFAULT_HEIGHT = 550; + private static final double DEFAULT_WIDTH = 880; private final double windowWidth; private final double windowHeight; diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..8e062c411d4 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -11,6 +11,7 @@ * Helper functions for handling strings. */ public class StringUtil { + public static final String SEPARATOR = "\n------------------------------------------\n"; /** * Returns true if the {@code sentence} contains the {@code word}. diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..eaf30fe0369 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -36,15 +36,32 @@ public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePref */ public static String format(Person person) { final StringBuilder builder = new StringBuilder(); - builder.append(person.getName()) - .append("; Phone: ") - .append(person.getPhone()) - .append("; Email: ") - .append(person.getEmail()) - .append("; Address: ") - .append(person.getAddress()) - .append("; Tags: "); + builder.append(person.getName()); + if (!person.getPhone().isEmpty()) { + builder.append("; Phone: ").append(person.getPhone()); + } + if (!person.getEmail().isEmpty()) { + builder.append("; Email: ").append(person.getEmail()); + } + if (!person.getAddress().isEmpty()) { + builder.append("; Address: ").append(person.getAddress()); + } + if (!person.getNote().isEmpty()) { + builder.append("; Note: ").append(person.getNote()); + } + builder.append("; Tags: "); person.getTags().forEach(builder::append); + + builder.append("; Appointments: "); + builder.append(person.getAppointments()); + + builder.append("; Subjects: "); + person.getSubjects().forEach(builder::append); + + if (!person.getLevel().isEmpty()) { + builder.append("; Level: ").append(person.getLevel()); + } + return builder.toString(); } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..483c29b9789 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -2,15 +2,21 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTE; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.List; + import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.appointment.AppointmentList; +import seedu.address.model.appointment.DisjointAppointmentList; import seedu.address.model.person.Person; /** @@ -26,17 +32,23 @@ public class AddCommand extends Command { + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" + + PREFIX_NOTE + "NOTE" + + "[" + PREFIX_APPOINTMENT + "APPOINTMENT]" + + "[" + 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_NOTE + "Went to fetch red paint " + + PREFIX_APPOINTMENT + "12:00-13:00 TUE " + + PREFIX_APPOINTMENT + "14:00-17:00 SUN " + 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"; + public static final String MESSAGE_NEAR_DUPLICATES = "New person added: %1$s \nPossible duplicate contacts: %2$s"; private final Person toAdd; @@ -56,10 +68,35 @@ public CommandResult execute(Model model) throws CommandException { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } + checkAppointmentOverlap(model); + + // Duplicate Detection feature + List duplicateNames = model.findNearDuplicates(toAdd); model.addPerson(toAdd); + + // If there are near duplicate names + if (!duplicateNames.isEmpty()) { + return new CommandResult(String.format(MESSAGE_NEAR_DUPLICATES, + Messages.format(toAdd), + String.join(", ", duplicateNames))); + } + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); } + private void checkAppointmentOverlap(Model model) throws CommandException { + // between appointments to be added and existing appointments + AppointmentList appointments = toAdd.getAppointments(); + if (model.appointmentsOverlap(appointments.asUnmodifiableObservableList())) { + throw new CommandException(DisjointAppointmentList.MESSAGE_CONSTRAINTS); + } + + // between two appointments to be added + if (appointments.isOverlapping()) { + throw new CommandException(DisjointAppointmentList.MESSAGE_CONSTRAINTS); + } + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..440d87a2419 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -2,9 +2,13 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEVEL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTE; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SUBJECT; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; @@ -14,6 +18,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.CollectionUtil; @@ -21,11 +26,15 @@ import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.appointment.AppointmentList; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.Level; import seedu.address.model.person.Name; +import seedu.address.model.person.Note; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Subject; import seedu.address.model.tag.Tag; /** @@ -43,7 +52,11 @@ public class EditCommand extends Command { + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_NOTE + "NOTE]" + + "[" + PREFIX_APPOINTMENT + "APPOINTMENT]" + + "[" + PREFIX_TAG + "TAG] ...\n" + + "[" + PREFIX_SUBJECT + "SUBJECT] " + + "[" + PREFIX_LEVEL + "LEVEL]\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " + PREFIX_EMAIL + "johndoe@example.com"; @@ -51,7 +64,9 @@ public class EditCommand extends Command { public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - + public static final String MESSAGE_NEAR_DUPLICATES = "Edited Person: %1$s \nPossible duplicate contacts: %2$s"; + public static final String MESSAGE_OVERLAPPING_APPOINTMENT = + "This person's appointments clash with an existing appointment"; private final Index index; private final EditPersonDescriptor editPersonDescriptor; @@ -83,8 +98,37 @@ public CommandResult execute(Model model) throws CommandException { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } + // Duplicate Detection feature + List duplicateNames = model.findNearDuplicates(editedPerson); + + // Overlapping appointment detection + if (editedPerson.getAppointments().isOverlapping()) { + throw new CommandException(MESSAGE_OVERLAPPING_APPOINTMENT); + } + + AppointmentList editedAppointmentList = new AppointmentList(); + editedAppointmentList.setAppointments(editedPerson.getAppointments()); + editedAppointmentList.addAll(model.getFilteredAppointmentList() + .stream() + .filter(appointment -> !(personToEdit.getAppointments().contains(appointment))) + .collect(Collectors.toList())); + + if (editedAppointmentList.isOverlapping()) { + throw new CommandException(MESSAGE_OVERLAPPING_APPOINTMENT); + } + model.setPerson(personToEdit, editedPerson); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + // If there are near duplicate names + if (!duplicateNames.isEmpty()) { + // remove the old name as it will be detected + duplicateNames.remove(personToEdit.getName().toString()); + return new CommandResult(String.format(MESSAGE_NEAR_DUPLICATES, + Messages.format(editedPerson), + String.join(", ", duplicateNames))); + } + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); } @@ -99,9 +143,17 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + Note updatedNote = editPersonDescriptor.getNote().orElse(personToEdit.getNote()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + AppointmentList updatedAppointments = editPersonDescriptor + .getAppointments().orElse(personToEdit.getAppointments()); + Set updatedSubjects = editPersonDescriptor.getSubjects().orElse(personToEdit.getSubjects()); + Level updatedLevel = editPersonDescriptor.getLevel().orElse(personToEdit.getLevel()); + + return new Person( + updatedName, updatedPhone, updatedEmail, updatedAddress, updatedNote, updatedTags, + updatedAppointments, updatedSubjects, updatedLevel + ); } @Override @@ -137,7 +189,11 @@ public static class EditPersonDescriptor { private Phone phone; private Email email; private Address address; + private Note note; private Set tags; + private AppointmentList appointments; + private Set subjects; + private Level level; public EditPersonDescriptor() {} @@ -150,14 +206,18 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setPhone(toCopy.phone); setEmail(toCopy.email); setAddress(toCopy.address); + setNote(toCopy.note); setTags(toCopy.tags); + setAppointments(toCopy.appointments); + setSubjects(toCopy.subjects); + setLevel(toCopy.level); } /** * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, address, note, tags, appointments, subjects, level); } public void setName(Name name) { @@ -192,6 +252,14 @@ public Optional
getAddress() { return Optional.ofNullable(address); } + public void setNote(Note note) { + this.note = note; + } + + public Optional getNote() { + return Optional.ofNullable(note); + } + /** * Sets {@code tags} to this object's {@code tags}. * A defensive copy of {@code tags} is used internally. @@ -208,6 +276,59 @@ public void setTags(Set tags) { public Optional> getTags() { return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); } + /** + * Sets {@code appointments} to this object's {@code appointments}. + * A defensive copy of {@code appointments} is used internally. + */ + public void setAppointments(AppointmentList appointments) { + if (appointments == null) { + this.appointments = null; + return; + } + this.appointments = new AppointmentList(); + this.appointments.addAll(appointments.asUnmodifiableObservableList()); + } + /** + * Returns an unmodifiable appointment set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code appointments} is null. + */ + public Optional getAppointments() { + if (appointments == null) { + return Optional.empty(); + } + AppointmentList defensiveCopy = new AppointmentList(); + defensiveCopy.addAll(appointments.asUnmodifiableObservableList()); + return Optional.of(defensiveCopy); + } + + /** + * Sets {@code subjects} to this object's {@code subjects}. + * A defensive copy of {@code subjects} is used internally. + */ + public void setSubjects(Set subjects) { + this.subjects = (subjects != null) ? new HashSet<>(subjects) : null; + } + + /** + * Returns an unmodifiable subject set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code subjects} is null. + */ + public Optional> getSubjects() { + return (subjects != null) ? Optional.of(Collections.unmodifiableSet(subjects)) : Optional.empty(); + } + + /** + * Sets {@code level} to this object's {@code level}. + */ + public void setLevel(Level level) { + this.level = level; + } + + public Optional getLevel() { + return Optional.ofNullable(level); + } @Override public boolean equals(Object other) { @@ -225,7 +346,11 @@ public boolean equals(Object other) { && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) && Objects.equals(address, otherEditPersonDescriptor.address) - && Objects.equals(tags, otherEditPersonDescriptor.tags); + && Objects.equals(note, otherEditPersonDescriptor.note) + && Objects.equals(tags, otherEditPersonDescriptor.tags) + && Objects.equals(appointments, otherEditPersonDescriptor.appointments) + && Objects.equals(subjects, otherEditPersonDescriptor.subjects) + && Objects.equals(level, otherEditPersonDescriptor.level); } @Override @@ -235,7 +360,11 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("note", note) + .add("appointments", appointments) .add("tags", tags) + .add("subjects", subjects) + .add("level", level) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/ViewAppointmentsCommand.java b/src/main/java/seedu/address/logic/commands/ViewAppointmentsCommand.java new file mode 100644 index 00000000000..4b3b990cfda --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewAppointmentsCommand.java @@ -0,0 +1,80 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javafx.util.Pair; +import seedu.address.model.Model; +import seedu.address.model.appointment.AppointmentIsDayOfWeekPredicate; +import seedu.address.model.person.Person; + +/** + * Shows all appointments in the filtered address book. + */ +public class ViewAppointmentsCommand extends Command { + + public static final String COMMAND_WORD = "appointments"; + + public static final String MESSAGE_SUCCESS = "Listed all appointments"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Lists all appointments of persons displayed.\n" + + "Example: " + COMMAND_WORD + "\n" + + "or\n" + + COMMAND_WORD + " [DAY_OF_WEEK]: Lists all appointments on the days of the week specified.\n" + + "Example: " + COMMAND_WORD + " MON\n, " + COMMAND_WORD + " MON TUE\n"; + + private final AppointmentIsDayOfWeekPredicate predicate; + + public ViewAppointmentsCommand(AppointmentIsDayOfWeekPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + // Get all appointments from the last shown list of persons + List appointments = lastShownList.stream() + .flatMap(person -> person.hasAppointments() + ? person.getAppointments() + .asUnmodifiableObservableList().stream() + .filter(predicate) + .map(appointment -> new Pair<>(appointment, person.getName().toString())) + : Stream.empty()) + .sorted((o1, o2) -> o1.getKey().compareTo(o2.getKey())) // comparing by appointment only + .map(pair -> pair.getValue() + ": " + pair.getKey().toString()) + .collect(Collectors.toList()); + + StringBuilder sb = new StringBuilder(); + sb.append("Appointments:\n"); + for (String appointment : appointments) { + sb.append(appointment).append("\n"); + } + + if (appointments.isEmpty()) { + sb.append("There are no appointments to show!"); + } + + return new CommandResult(sb.toString().trim()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ViewAppointmentsCommand)) { + return false; + } + + ViewAppointmentsCommand otherCommand = (ViewAppointmentsCommand) other; + return predicate.equals(otherCommand.predicate); + } +} 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..82c5087f6b7 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewCommand.java @@ -0,0 +1,72 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Displays all information of a contact in resultDisplay based on it's displayed index. + * + * Code is heavily borrowed from existing DeleteCommand.java due to extreme similarities in desired implementation. + */ +public class ViewCommand extends Command { + public static final String COMMAND_WORD = "view"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays all information about a person " + + "identified by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + private final Index targetIndex; + + public ViewCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToView = lastShownList.get(targetIndex.getZeroBased()); + List viewDetails = personToView.getViewDetails(); + StringBuilder sb = new StringBuilder(); + for (String detail : viewDetails) { + sb.append(detail); + } + return new CommandResult(sb.toString().trim()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ViewCommand)) { + return false; + } + + ViewCommand otherViewCommand = (ViewCommand) other; + return targetIndex.equals(otherViewCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..b3cc3fa8de4 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -1,10 +1,15 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.INCORRECT_PREFIX_MAP; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEVEL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTE; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SUBJECT; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Set; @@ -12,11 +17,15 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.AppointmentList; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.Level; import seedu.address.model.person.Name; +import seedu.address.model.person.Note; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Subject; import seedu.address.model.tag.Tag; /** @@ -31,21 +40,26 @@ public class AddCommandParser implements Parser { */ 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, INCORRECT_PREFIX_MAP, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_NOTE, PREFIX_TAG, PREFIX_APPOINTMENT, PREFIX_SUBJECT, PREFIX_LEVEL); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + if (!arePrefixesPresent(argMultimap, PREFIX_NAME) || !argMultimap.getPreamble().isEmpty()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).orElse(null)); + Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).orElse(null)); + Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).orElse(null)); + Note note = ParserUtil.parseNote(argMultimap.getValue(PREFIX_NOTE).orElse(null)); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + AppointmentList appointmentList = ParserUtil.parseAppointments(argMultimap.getAllValues(PREFIX_APPOINTMENT)); + Set subjectList = ParserUtil.parseSubjects(argMultimap.getAllValues(PREFIX_SUBJECT)); + Level level = ParserUtil.parseLevel(argMultimap.getValue(PREFIX_LEVEL).orElse(null)); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, email, address, note, tagList, appointmentList, subjectList, level); return new AddCommand(person); } diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..f97c8dcff7c 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -17,6 +17,8 @@ import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ViewAppointmentsCommand; +import seedu.address.logic.commands.ViewCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -77,6 +79,12 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case ViewAppointmentsCommand.COMMAND_WORD: + return new ViewAppointmentsCommandParser().parse(arguments); + + case ViewCommand.COMMAND_WORD: + return new ViewCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..f4619e0e1d3 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java @@ -2,7 +2,9 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -19,13 +21,37 @@ public class ArgumentTokenizer { * Tokenizes an arguments string and returns an {@code ArgumentMultimap} object that maps prefixes to their * respective argument values. Only the given prefixes will be recognized in the arguments string. * - * @param argsString Arguments string of the form: {@code preamble value value ...} - * @param prefixes Prefixes to tokenize the arguments string with - * @return ArgumentMultimap object that maps prefixes to their arguments + * @param argsString Arguments string of the form: {@code preamble value value ...} + * @param incorrectPrefixes A list of incorrect prefixes to check against. + * @param prefixes Prefixes to tokenize the arguments string with + * @return ArgumentMultimap object that maps prefixes to their arguments */ - public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) { - List positions = findAllPrefixPositions(argsString, prefixes); - return extractArguments(argsString, positions); + public static ArgumentMultimap tokenize(String argsString, HashMap> incorrectPrefixes, Prefix... prefixes) { + String correctedArgsString = fixIncorrectPrefixes(argsString, incorrectPrefixes); + List positions = findAllPrefixPositions(correctedArgsString, prefixes); + return extractArguments(correctedArgsString, positions); + } + + /** + * Corrects incorrect prefixes in an arguments string using a provided mapping. + * + * @param argsString The string containing potentially incorrect prefixes. + * @param incorrectPrefixes A map of correct prefixes to their respective list of common incorrect variations. + * @return The corrected arguments string with all known incorrect prefixes replaced by their correct versions. + */ + public static String fixIncorrectPrefixes(String argsString, HashMap> incorrectPrefixes) { + // Iterate over each entry in the incorrectPrefixes map + for (Map.Entry> entry : incorrectPrefixes.entrySet()) { + Prefix correctPrefix = entry.getKey(); + List incorrectPrefixList = entry.getValue(); + + // Replace each incorrect prefix with the correct one + for (String incorrectPrefix : incorrectPrefixList) { + argsString = argsString.replace(incorrectPrefix, correctPrefix.getPrefix()); + } + } + return argsString; } /** diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..4dc5a9bc3ef 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -1,5 +1,10 @@ package seedu.address.logic.parser; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + + /** * Contains Command Line Interface (CLI) syntax definitions common to multiple commands */ @@ -10,6 +15,33 @@ public class CliSyntax { public static final Prefix PREFIX_PHONE = new Prefix("p/"); public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_NOTE = new Prefix("nt/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_APPOINTMENT = new Prefix("ap/"); + public static final Prefix PREFIX_SUBJECT = new Prefix("s/"); + public static final Prefix PREFIX_LEVEL = new Prefix("l/"); + + /* + Incorrect but acceptable prefixes. We may add more as required. + */ + public static final HashMap> INCORRECT_PREFIX_MAP = new HashMap<>(); + + static { + // Populate the map with common incorrect prefixes + INCORRECT_PREFIX_MAP.put(PREFIX_NAME, Arrays.asList("name/", "nae/", "nam/")); + INCORRECT_PREFIX_MAP.put(PREFIX_PHONE, Arrays.asList("phone/", "phon/", "pho/", "ph/", "hp/", "/handphone")); + INCORRECT_PREFIX_MAP.put(PREFIX_EMAIL, Arrays.asList("email/", "emai/", "eml/", "em/", "ema/")); + INCORRECT_PREFIX_MAP.put(PREFIX_ADDRESS, Arrays.asList("address/", "addr/", "add/", + "ad/", "addres/", "adress/")); + INCORRECT_PREFIX_MAP.put(PREFIX_NOTE, Arrays.asList("note/", "not/", "nt/")); + INCORRECT_PREFIX_MAP.put(PREFIX_TAG, Arrays.asList("tag/", "ta/", "tg/")); + INCORRECT_PREFIX_MAP.put(PREFIX_APPOINTMENT, Arrays.asList("appointment/", "appt/", "apt/", + "appoint/", "app/", "appointmen/")); + INCORRECT_PREFIX_MAP.put(PREFIX_SUBJECT, Arrays.asList("subject/", "subj/", "sub/", + "subjec/", "subjet/", "subje/", "su/", "ubject/")); + INCORRECT_PREFIX_MAP.put(PREFIX_LEVEL, Arrays.asList("level/", "lvl/", "leve/", + "lv/", "le/", "lev/", "lvel/", "evel/")); + } + } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..fcb9aa684b0 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -2,10 +2,15 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.INCORRECT_PREFIX_MAP; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LEVEL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NOTE; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SUBJECT; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Collection; @@ -17,6 +22,8 @@ import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.AppointmentList; +import seedu.address.model.appointment.exceptions.OverlappingAppointmentException; import seedu.address.model.tag.Tag; /** @@ -32,7 +39,8 @@ 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, INCORRECT_PREFIX_MAP, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_NOTE, PREFIX_TAG, PREFIX_APPOINTMENT, PREFIX_SUBJECT, PREFIX_LEVEL); Index index; @@ -50,15 +58,28 @@ public EditCommand parse(String args) throws ParseException { editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); } if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); + editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).orElse(null))); } if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).orElse(null))); } if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).orElse(null))); + } + if (argMultimap.getValue(PREFIX_NOTE).isPresent()) { + editPersonDescriptor.setNote(ParserUtil.parseNote(argMultimap.getValue(PREFIX_NOTE).get())); + } + + if (argMultimap.getValue(PREFIX_SUBJECT).isPresent()) { + editPersonDescriptor.setSubjects(ParserUtil.parseSubjects(argMultimap.getAllValues(PREFIX_SUBJECT))); + } + + if (argMultimap.getValue(PREFIX_LEVEL).isPresent()) { + editPersonDescriptor.setLevel(ParserUtil.parseLevel(argMultimap.getValue(PREFIX_LEVEL).orElse(null))); } parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + parseAppointmentsForEdit(argMultimap.getAllValues(PREFIX_APPOINTMENT)) + .ifPresent(editPersonDescriptor::setAppointments); if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); @@ -81,5 +102,22 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; return Optional.of(ParserUtil.parseTags(tagSet)); } + /** + * Parses {@code Collection appointments} into an {@code AppointmentList} + * if {@code appointments} is non-empty. + * If {@code appointments} contain only one element which is an empty string, it will be parsed into a + * {@code AppointmentList} containing zero appointments. + */ + private Optional parseAppointmentsForEdit(Collection appointments) throws ParseException, + OverlappingAppointmentException { + assert appointments != null; + + if (appointments.isEmpty()) { + return Optional.empty(); + } + Collection appointmentSet = + appointments.size() == 1 && appointments.contains("") ? Collections.emptySet() : appointments; + return Optional.of(ParserUtil.parseAppointments(appointmentSet)); + } } diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..2be5096c3bf 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -9,10 +9,20 @@ import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.AppointmentList; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.EmptyAddress; +import seedu.address.model.person.EmptyEmail; +import seedu.address.model.person.EmptyLevel; +import seedu.address.model.person.EmptyNote; +import seedu.address.model.person.EmptyPhone; +import seedu.address.model.person.Level; import seedu.address.model.person.Name; +import seedu.address.model.person.Note; import seedu.address.model.person.Phone; +import seedu.address.model.person.Subject; import seedu.address.model.tag.Tag; /** @@ -57,8 +67,13 @@ public static Name parseName(String name) throws ParseException { * @throws ParseException if the given {@code phone} is invalid. */ public static Phone parsePhone(String phone) throws ParseException { - requireNonNull(phone); + if (phone == null) { + return new EmptyPhone(); + } String trimmedPhone = phone.trim(); + if (phone.equals("")) { + return new EmptyPhone(); + } if (!Phone.isValidPhone(trimmedPhone)) { throw new ParseException(Phone.MESSAGE_CONSTRAINTS); } @@ -72,8 +87,13 @@ public static Phone parsePhone(String phone) throws ParseException { * @throws ParseException if the given {@code address} is invalid. */ public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); + if (address == null) { + return new EmptyAddress(); + } String trimmedAddress = address.trim(); + if (address.equals("")) { + return new EmptyAddress(); + } if (!Address.isValidAddress(trimmedAddress)) { throw new ParseException(Address.MESSAGE_CONSTRAINTS); } @@ -87,8 +107,13 @@ public static Address parseAddress(String address) throws ParseException { * @throws ParseException if the given {@code email} is invalid. */ public static Email parseEmail(String email) throws ParseException { - requireNonNull(email); + if (email == null) { + return new EmptyEmail(); + } String trimmedEmail = email.trim(); + if (email.equals("")) { + return new EmptyEmail(); + } if (!Email.isValidEmail(trimmedEmail)) { throw new ParseException(Email.MESSAGE_CONSTRAINTS); } @@ -110,6 +135,23 @@ public static Tag parseTag(String tag) throws ParseException { return new Tag(trimmedTag); } + /** + * Parses a {@code String note} into a {@code Note}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code note} is invalid. + */ + public static Note parseNote(String note) throws ParseException { + if (note == null) { + return new EmptyNote(); + } + String trimmedNote = note.trim(); + if (note.equals("")) { + return new EmptyNote(); + } + return new Note(trimmedNote); + } + /** * Parses {@code Collection tags} into a {@code Set}. */ @@ -117,8 +159,96 @@ public static Set parseTags(Collection tags) throws ParseException requireNonNull(tags); final Set tagSet = new HashSet<>(); for (String tagName : tags) { + if (tagName.isEmpty()) { + continue; + } tagSet.add(parseTag(tagName)); } return tagSet; } + /** + * Parses a {@code String appointment} into a {@code Appointment}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code appointment} is invalid. + */ + public static Appointment parseAppointment(String appointment) throws ParseException { + requireNonNull(appointment); + String trimmedAppointment = appointment.trim(); + if (!Appointment.isValidAppointment(trimmedAppointment)) { + throw new ParseException(Appointment.MESSAGE_CONSTRAINTS); + } + return new Appointment(trimmedAppointment); + } + /** + * Parses {@code Collection appointment} into an {@code AppointmentList}. + */ + public static AppointmentList parseAppointments(Collection appointments) throws ParseException { + requireNonNull(appointments); + final AppointmentList appointmentList = new AppointmentList(); + for (String ap : appointments) { + if (ap.isEmpty()) { + continue; + } + Appointment appointment = parseAppointment(ap); + /*if (appointmentList.overlaps(appointment)) { + throw new ParseException(DisjointAppointmentList.MESSAGE_CONSTRAINTS); + }*/ + appointmentList.add(appointment); + } + return appointmentList; + } + + /** + * Parses a {@code String subject} into a {@code Subject}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code subject} is invalid. + */ + private static Subject parseSubject(String subject) throws ParseException { + if (subject == null) { + return null; + } + String trimmedSubject = subject.trim(); + if (trimmedSubject.equals("")) { + return null; + } + if (!Subject.isValidSubject(trimmedSubject)) { + throw new ParseException(Subject.MESSAGE_CONSTRAINTS); + } + return new Subject(trimmedSubject); + } + + /** + * Parses {@code Collection subjects} into a {@code Set}. + */ + public static Set parseSubjects(Collection subjects) throws ParseException { + requireNonNull(subjects); + final Set subjectSet = new HashSet<>(); + for (String subjectName : subjects) { + Subject s = parseSubject(subjectName); + if (s == null) { + continue; + } + subjectSet.add(s); + } + return subjectSet; + } + + /** + * Parses a {@code String level} into a {@code Level}. + */ + public static Level parseLevel(String level) throws ParseException { + if (level == null) { + return new EmptyLevel(); + } + String trimmedLevel = level.trim(); + if (trimmedLevel.equals("")) { + return new EmptyLevel(); + } + if (!Level.isValidLevel(level)) { + throw new ParseException(Level.MESSAGE_CONSTRAINTS); + } + return new Level(level); + } } diff --git a/src/main/java/seedu/address/logic/parser/ViewAppointmentsCommandParser.java b/src/main/java/seedu/address/logic/parser/ViewAppointmentsCommandParser.java new file mode 100644 index 00000000000..a04817f7f76 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ViewAppointmentsCommandParser.java @@ -0,0 +1,53 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.time.DayOfWeek; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.logic.commands.ViewAppointmentsCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.AppointmentIsDayOfWeekPredicate; + +/** + * Parses input arguments and creates a new ViewAppointmentsCommand object. + * Code is borrowed from existing ViewCommandParser.java due to similarities in desired implementation. + */ +public class ViewAppointmentsCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of ViewAppointmentsCommand and returns + * a ViewAppointmentsCommand object for execution. + * @throws ParseException if the user input does not conform to expected format. + */ + public ViewAppointmentsCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim().toUpperCase(); + if (trimmedArgs.isEmpty()) { + + // add all days of the week to the list + List defaultDayOfWeeks = new ArrayList<>(Appointment.DAY_OF_WEEK_TO_NUM.keySet()); + return new ViewAppointmentsCommand(new AppointmentIsDayOfWeekPredicate(defaultDayOfWeeks)); + } + + String[] days = trimmedArgs.split("\\s+"); + List dayList = Stream.of(days).map(String::toUpperCase).collect(Collectors.toList()); + + // check that all day in dayList are valid day of the week + for (String day : dayList) { + if (!Appointment.DAY_TO_DAY_OF_WEEK.containsKey(day)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewAppointmentsCommand.MESSAGE_USAGE)); + } + } + + List dayOfWeekList = dayList.stream() + .map(Appointment.DAY_TO_DAY_OF_WEEK::get) + .distinct() + .collect(Collectors.toList()); + + return new ViewAppointmentsCommand(new AppointmentIsDayOfWeekPredicate(dayOfWeekList)); + } +} 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..477dac9816f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ViewCommandParser.java @@ -0,0 +1,31 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ViewCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ViewCommand object. + * + * Code is heavily borrowed from existing DeleteCommandParser.java due to similarities in desired implementation. + */ +public class ViewCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of ViewCommand and returns + * a ViewCommand object for execution. + * + * @throws ParseException if the user input does not conform to 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( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE), + pe); + } + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..60372d84c9c 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -2,10 +2,14 @@ import static java.util.Objects.requireNonNull; +import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.DisjointAppointmentList; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; @@ -15,6 +19,7 @@ */ public class AddressBook implements ReadOnlyAddressBook { + private final DisjointAppointmentList appointments; private final UniquePersonList persons; /* @@ -26,6 +31,7 @@ public class AddressBook implements ReadOnlyAddressBook { */ { persons = new UniquePersonList(); + appointments = new DisjointAppointmentList(); } public AddressBook() {} @@ -43,9 +49,17 @@ public AddressBook(ReadOnlyAddressBook toBeCopied) { /** * Replaces the contents of the person list with {@code persons}. * {@code persons} must not contain duplicate persons. + * */ public void setPersons(List persons) { this.persons.setPersons(persons); + this.appointments.setAppointments(persons + .stream() + .flatMap(person -> person.getAppointments() + .asUnmodifiableObservableList() + .stream()) + .collect(Collectors.toList())); + this.appointments.sort(); } /** @@ -53,8 +67,8 @@ public void setPersons(List persons) { */ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); - setPersons(newData.getPersonList()); + this.appointments.sort(); } //// person-level operations @@ -67,12 +81,24 @@ public boolean hasPerson(Person person) { return persons.contains(person); } + /** + * Returns a list of persons with similar name as {@code person}. + */ + public List findNearDuplicates(Person person) { + requireNonNull(person); + return persons.findNearDuplicates(person); + } + /** * Adds a person to the address book. * The person must not already exist in the address book. */ public void addPerson(Person p) { persons.add(p); + for (Appointment appointment : p.getAppointments()) { + addAppointment(appointment); + } + this.appointments.sort(); } /** @@ -84,6 +110,17 @@ public void setPerson(Person target, Person editedPerson) { requireNonNull(editedPerson); persons.setPerson(target, editedPerson); + + // remove target's appointments + for (Appointment appointment : target.getAppointments()) { + appointments.remove(appointment); + } + + // add editedPerson's appointments + for (Appointment appointment : editedPerson.getAppointments()) { + addAppointment(appointment); + } + this.appointments.sort(); } /** @@ -92,6 +129,56 @@ public void setPerson(Person target, Person editedPerson) { */ public void removePerson(Person key) { persons.remove(key); + + // remove key's appointments + for (Appointment appointment : key.getAppointments()) { + appointments.remove(appointment); + } + + this.appointments.sort(); + } + + //// appointment-level operations + /** + * Adds an appointment to the address book. + * The appointment must not overlap with existing appointments in the address book. + */ + public void addAppointment(Appointment appointment) { + appointments.add(appointment); + this.appointments.sort(); + } + + /** + * Replaces the given appointment {@code target} in the list with {@code editedAppointment}. + * {@code target} must exist in the address book. + * The appointment {@code editedAppointment} must not overlap with other existing appointments in the address book. + */ + public void setAppointment(Appointment target, Appointment editedAppointment) { + requireNonNull(editedAppointment); + + appointments.setAppointment(target, editedAppointment); + this.appointments.sort(); + } + + /** + * Returns true if an appointment {@code appointment} overlaps with existing appointments in the address book. + */ + public boolean appointmentsOverlap(Appointment appointment) { + requireNonNull(appointment); + return appointments.overlaps(appointment); + } + + /** + * Returns true if an appointment in {@code appointments} overlaps with existing appointments in the address book. + */ + public boolean appointmentsOverlap(Collection appointments) { + requireNonNull(appointments); + for (Appointment ap : appointments) { + if (this.appointments.overlaps(ap)) { + return true; + } + } + return false; } //// util methods @@ -108,6 +195,11 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList getAppointmentList() { + return appointments.asUnmodifiableObservableList(); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -120,7 +212,9 @@ public boolean equals(Object other) { } AddressBook otherAddressBook = (AddressBook) other; - return persons.equals(otherAddressBook.persons); + boolean isPersonsEqual = persons.equals(otherAddressBook.persons); + boolean isAppointmentsEqual = appointments.equals(otherAddressBook.appointments); + return isPersonsEqual && isAppointmentsEqual; } @Override diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..64436c8646e 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,10 +1,13 @@ package seedu.address.model; import java.nio.file.Path; +import java.util.Collection; +import java.util.List; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; /** @@ -57,6 +60,12 @@ public interface Model { */ boolean hasPerson(Person person); + /** + * Returns a list of persons with similar name as {@code person}. + */ + List findNearDuplicates(Person person); + + /** * Deletes the given person. * The person must exist in the address book. @@ -76,12 +85,31 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); + /** + * Returns true if an existing appointment overlaps with any appointment in {@code appointments}. + */ + boolean appointmentsOverlap(Collection appointments); + + /** + * Returns true if an existing appointment overlaps with {@code appointment}. + */ + boolean appointmentsOverlap(Appointment appointment); + /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered appointment list */ + ObservableList getFilteredAppointmentList(); + /** * Updates the filter of the filtered person list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Updates the filter of the filtered Appointment list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredAppointmentList(Predicate predicate); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..8fcaac5caff 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,6 +4,8 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Collection; +import java.util.List; import java.util.function.Predicate; import java.util.logging.Logger; @@ -11,6 +13,7 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; /** @@ -22,6 +25,7 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final FilteredList filteredAppointments; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -34,6 +38,7 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredAppointments = new FilteredList<>(this.addressBook.getAppointmentList()); } public ModelManager() { @@ -76,7 +81,6 @@ public void setAddressBookFilePath(Path addressBookFilePath) { } //=========== AddressBook ================================================================================ - @Override public void setAddressBook(ReadOnlyAddressBook addressBook) { this.addressBook.resetData(addressBook); @@ -87,12 +91,22 @@ public ReadOnlyAddressBook getAddressBook() { return addressBook; } + //// person functionality @Override public boolean hasPerson(Person person) { requireNonNull(person); return addressBook.hasPerson(person); } + /** + * Returns a list of persons with similar name as {@code person}. + */ + @Override + public List findNearDuplicates(Person person) { + requireNonNull(person); + return addressBook.findNearDuplicates(person); + } + @Override public void deletePerson(Person target) { addressBook.removePerson(target); @@ -101,6 +115,7 @@ public void deletePerson(Person target) { @Override public void addPerson(Person person) { addressBook.addPerson(person); + updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); } @@ -111,6 +126,20 @@ public void setPerson(Person target, Person editedPerson) { addressBook.setPerson(target, editedPerson); } + //// appointment functionality + @Override + public boolean appointmentsOverlap(Appointment appointment) { + requireNonNull(appointment); + return addressBook.appointmentsOverlap(appointment); + } + + @Override + public boolean appointmentsOverlap(Collection appointments) { + requireNonNull(appointments); + return addressBook.appointmentsOverlap(appointments); + } + + //=========== Filtered Person List Accessors ============================================================= /** @@ -128,6 +157,23 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } + //=========== Filtered Appointment List Accessors ============================================================= + + /** + * Returns an unmodifiable view of the list of {@code Appointment} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList getFilteredAppointmentList() { + return filteredAppointments; + } + + @Override + public void updateFilteredAppointmentList(Predicate predicate) { + requireNonNull(predicate); + filteredAppointments.setPredicate(predicate); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -142,7 +188,8 @@ public boolean equals(Object other) { ModelManager otherModelManager = (ModelManager) other; return addressBook.equals(otherModelManager.addressBook) && userPrefs.equals(otherModelManager.userPrefs) - && filteredPersons.equals(otherModelManager.filteredPersons); + && filteredPersons.equals(otherModelManager.filteredPersons) + && filteredAppointments.equals(otherModelManager.filteredAppointments); } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..a4304f2778b 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,6 +1,7 @@ package seedu.address.model; import javafx.collections.ObservableList; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; /** @@ -14,4 +15,10 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + /** + * Returns an unmodifiable view of the appointments list. + * This list will not contain any overlapping appointments. + */ + ObservableList getAppointmentList(); + } diff --git a/src/main/java/seedu/address/model/appointment/Appointment.java b/src/main/java/seedu/address/model/appointment/Appointment.java new file mode 100644 index 00000000000..e80ba9d637c --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/Appointment.java @@ -0,0 +1,209 @@ +package seedu.address.model.appointment; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +/** + * Represents a Person's appointment in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidAppointment(String)} + */ +public class Appointment implements Comparable { + public static final String MESSAGE_CONSTRAINTS = "Appointment should be of the format 'HH:MM-HH:MM DAY' " + + "and adhere to the following constraints:\n" + + "1. HH:MM follows 24 hour time; " + + "HH is from 00 to 23, " + + "MM is from 00 to 59.\n" + + "2. This is followed by a DAY. " + + "DAY must be one of: 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT','SUN'\n"; + public static final HashMap DAY_TO_DAY_OF_WEEK; + public static final HashMap DAY_OF_WEEK_TO_NUM; + // alphanumeric and special characters + private static final String HOUR = "[\\d]{2}"; + private static final String MINUTE = "[\\d]{2}"; + private static final String START_TIME = HOUR + ":" + MINUTE; + private static final String END_TIME = HOUR + ":" + MINUTE; + private static final String DAY = "[A-z]{3}"; + public static final String VALIDATION_REGEX = START_TIME + "-" + END_TIME + "[\\s]+" + DAY; + + // initialize map from String to DayOfWeek + static { + DAY_TO_DAY_OF_WEEK = new HashMap<>(); + DAY_TO_DAY_OF_WEEK.put("MON", DayOfWeek.MONDAY); + DAY_TO_DAY_OF_WEEK.put("TUE", DayOfWeek.TUESDAY); + DAY_TO_DAY_OF_WEEK.put("WED", DayOfWeek.WEDNESDAY); + DAY_TO_DAY_OF_WEEK.put("THU", DayOfWeek.THURSDAY); + DAY_TO_DAY_OF_WEEK.put("FRI", DayOfWeek.FRIDAY); + DAY_TO_DAY_OF_WEEK.put("SAT", DayOfWeek.SATURDAY); + DAY_TO_DAY_OF_WEEK.put("SUN", DayOfWeek.SUNDAY); + + DAY_OF_WEEK_TO_NUM = new HashMap<>(); + DAY_OF_WEEK_TO_NUM.put(DayOfWeek.MONDAY, 1); + DAY_OF_WEEK_TO_NUM.put(DayOfWeek.TUESDAY, 2); + DAY_OF_WEEK_TO_NUM.put(DayOfWeek.WEDNESDAY, 3); + DAY_OF_WEEK_TO_NUM.put(DayOfWeek.THURSDAY, 4); + DAY_OF_WEEK_TO_NUM.put(DayOfWeek.FRIDAY, 5); + DAY_OF_WEEK_TO_NUM.put(DayOfWeek.SATURDAY, 6); + DAY_OF_WEEK_TO_NUM.put(DayOfWeek.SUNDAY, 7); + } + + public final String value; + private final LocalTime startTime; + private final LocalTime endTime; + private final DayOfWeek day; + + /** + * Constructs a {@code Appointment}. + * + * @param appointment A valid appointment. + */ + public Appointment(String appointment) { + requireNonNull(appointment); + checkArgument(isValidAppointment(appointment), MESSAGE_CONSTRAINTS); + + value = appointment.toUpperCase(); + startTime = LocalTime.parse(extractStartTime(appointment)); + endTime = LocalTime.parse(extractEndTime(appointment)); + day = DAY_TO_DAY_OF_WEEK.get(extractDay(appointment)); + } + + /** + * Returns true if a given collection of appointments overlap. + */ + public static boolean hasOverlapping(Collection appointments) { + List appointmentList = new ArrayList<>(appointments); + int size = appointmentList.size(); + for (int i = 0; i < size - 1; i += 1) { + for (int j = i + 1; j < size; j += 1) { + Appointment appointment = appointmentList.get(i); + Appointment other = appointmentList.get(j); + if (appointment.overlapsWith(other)) { + return true; + } + } + } + return false; + } + + /** + * Returns true if a given string is an appointment. + */ + public static boolean isValidAppointment(String test) { + if (!(test.matches(VALIDATION_REGEX))) { + return false; + } + + boolean isStartTimeValid = isValidTime(extractStartTime(test)); + boolean isEndTimeValid = isValidTime(extractEndTime(test)); + + if (!isStartTimeValid || !isEndTimeValid) { + return false; + } + + LocalTime startTime = LocalTime.parse(extractStartTime(test)); + LocalTime endTime = LocalTime.parse(extractEndTime(test)); + boolean isStartTimeBeforeEndTime = startTime.isBefore(endTime); + + String day = extractDay(test); + boolean isDayValid = DAY_TO_DAY_OF_WEEK.containsKey(day); + + return isStartTimeBeforeEndTime && isDayValid; + } + + private static boolean isValidTime(String appointment) { + int hour = Integer.parseInt(appointment.substring(0, 2)); + int minute = Integer.parseInt(appointment.substring(3, 5)); + + assert(hour > -1); + assert(minute > -1); + boolean hourValid = hour < 24; + boolean minuteValid = minute < 60; + + return hourValid && minuteValid; + } + + private static String extractStartTime(String appointment) { + return appointment.substring(0, 5); + } + + private static String extractEndTime(String appointment) { + return appointment.substring(6, 11); + } + + private static String extractDay(String appointment) { + return appointment.substring(12).trim().toUpperCase(); + } + + public LocalTime getStartTime() { + return startTime; + } + + public LocalTime getEndTime() { + return endTime; + } + + public DayOfWeek getDay() { + return day; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Appointment)) { + return false; + } + + Appointment otherAppointment = (Appointment) other; + return value.equals(otherAppointment.value); + } + + /** + * Return true if appointment overlaps with other, otherwise False + */ + public boolean overlapsWith(Appointment other) { + // days are different + if (this.day != other.day) { + return false; + } + + // intervals overlap + if (this.startTime.isBefore(other.endTime) && other.startTime.isBefore(this.endTime)) { + return true; + } else if (other.startTime.isBefore(this.endTime) && this.startTime.isBefore(other.endTime)) { + return true; + } + + return false; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public int compareTo(Appointment o) { + if (DAY_OF_WEEK_TO_NUM.get(this.day) < DAY_OF_WEEK_TO_NUM.get(o.day)) { + return -1; + } else if (DAY_OF_WEEK_TO_NUM.get(this.day) > DAY_OF_WEEK_TO_NUM.get(o.day)) { + return 1; + } else { + return this.startTime.compareTo(o.startTime); + } + } +} diff --git a/src/main/java/seedu/address/model/appointment/AppointmentIsDayOfWeekPredicate.java b/src/main/java/seedu/address/model/appointment/AppointmentIsDayOfWeekPredicate.java new file mode 100644 index 00000000000..d004a3093f3 --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/AppointmentIsDayOfWeekPredicate.java @@ -0,0 +1,43 @@ +package seedu.address.model.appointment; + +import java.time.DayOfWeek; +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class AppointmentIsDayOfWeekPredicate implements Predicate { + private final List days; + + public AppointmentIsDayOfWeekPredicate(List dayOfWeeks) { + this.days = dayOfWeeks; + } + + @Override + public boolean test(Appointment appointment) { + return days.stream().anyMatch(day -> day.equals(appointment.getDay())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof seedu.address.model.appointment.AppointmentIsDayOfWeekPredicate)) { + return false; + } + + AppointmentIsDayOfWeekPredicate otherPredicate = (AppointmentIsDayOfWeekPredicate) other; + return this.days.equals(otherPredicate.days); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("days", days).toString(); + } +} diff --git a/src/main/java/seedu/address/model/appointment/AppointmentList.java b/src/main/java/seedu/address/model/appointment/AppointmentList.java new file mode 100644 index 00000000000..4d5897e3a39 --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/AppointmentList.java @@ -0,0 +1,145 @@ +package seedu.address.model.appointment; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.appointment.exceptions.AppointmentNotFoundException; + +/** + * A list of appointments that does not allow nulls. + * Supports a minimal set of list operations. + */ +public class AppointmentList implements Iterable { + protected final ObservableList internalList = FXCollections.observableArrayList(); + protected final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Adds an appointment to the list. + * The appointment must not already exist in the list. + */ + public void add(Appointment toAdd) { + requireNonNull(toAdd); + internalList.add(toAdd); + } + + /** + * Replaces the appointment {@code target} in the list with {@code editedAppointment}. + * {@code target} must exist in the appointment list. + */ + public void setAppointment(Appointment target, Appointment editedAppointment) { + requireAllNonNull(target, editedAppointment); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new AppointmentNotFoundException(); + } + + internalList.set(index, editedAppointment); + } + + /** + * Removes the equivalent appointment from the list. + * The appointment must exist in the list. + */ + public void remove(Appointment toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new AppointmentNotFoundException(); + } + } + + /** + * Replaces the contents of this list with {@code appointments}. + */ + public void setAppointments(AppointmentList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code appointments}. + */ + public void setAppointments(Collection appointments) { + requireAllNonNull(appointments); + internalList.setAll(appointments); + } + + /** + * Sorts the appointment list by the appointment's natural comparator. + */ + public void sort() { + internalList.sort(null); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AppointmentList)) { + return false; + } + + AppointmentList otherAppointmentList = (AppointmentList) other; + return internalList.equals(otherAppointmentList.internalList); + } + + @Override + public String toString() { + List appointmentStrings = internalList.stream() + .map(Appointment::toString).collect(Collectors.toList()); + return String.join(", ", appointmentStrings); + + } + + /** + * Returns true if the list contains an equivalent appointment as the given argument. + */ + public boolean contains(Appointment toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::equals); + } + + /** + * Returns true if the list has overlap between appointments. + */ + public boolean isOverlapping() { + return Appointment.hasOverlapping(internalList); + } + + /** + * Add all appointments in {@code appointments} to the list. + */ + public void addAll(Collection appointments) { + internalList.addAll(appointments); + } + + /** + * Returns true if there are no appointments in the list. + */ + public boolean isEmpty() { + return internalList.isEmpty(); + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } +} diff --git a/src/main/java/seedu/address/model/appointment/DisjointAppointmentList.java b/src/main/java/seedu/address/model/appointment/DisjointAppointmentList.java new file mode 100644 index 00000000000..7975e0a4e1a --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/DisjointAppointmentList.java @@ -0,0 +1,124 @@ +package seedu.address.model.appointment; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collection; +import java.util.Iterator; + +import seedu.address.model.appointment.exceptions.AppointmentNotFoundException; +import seedu.address.model.appointment.exceptions.OverlappingAppointmentException; + +/** + * A list of appointments that enforces no overlapping between its elements and does not allow nulls. + * Supports a minimal set of list operations. + */ +public class DisjointAppointmentList extends AppointmentList { + public static final String MESSAGE_CONSTRAINTS = + "This person's appointments clash with an existing appointment"; + + /** + * Returns true if the list contains an appointment overlapping wth the given argument. + */ + public boolean overlaps(Appointment toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::overlapsWith); + } + + @Override + public boolean isOverlapping() { + return false; + } + + /** + * Adds an appointment {@code toAdd} to the list. + * This appointment must not overlap with existing appointments in the list. + */ + @Override + public void add(Appointment toAdd) { + requireNonNull(toAdd); + if (overlaps(toAdd)) { + throw new OverlappingAppointmentException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the appointment {@code target} in the list with {@code editedAppointment}. + * {@code target} must exist in the appointment list. + * {@code editedAppointment} must not overlap with another existing appointment in the list. + */ + @Override + public void setAppointment(Appointment target, Appointment editedAppointment) { + requireAllNonNull(target, editedAppointment); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new AppointmentNotFoundException(); + } + + if (overlaps(editedAppointment)) { + throw new OverlappingAppointmentException(); + } + + internalList.set(index, editedAppointment); + } + + /** + * Removes the equivalent appointment from the list. + * The appointment must exist in the list. + */ + public void remove(Appointment toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new AppointmentNotFoundException(); + } + } + + /** + * Replaces the contents of this list with {@code appointments}. + */ + public void setAppointments(DisjointAppointmentList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code appointments}. + * {@code appointments} must not contain overlapping appointments. + */ + @Override + public void setAppointments(Collection appointments) { + requireAllNonNull(appointments); + if (Appointment.hasOverlapping(appointments)) { + throw new OverlappingAppointmentException(); + } + + internalList.setAll(appointments); + } + + /** + * The list {@code appointments} must not have any appointments that overlap with existing appointments + * and also overlap with each other. + */ + public void addAll(Collection appointments) { + if (Appointment.hasOverlapping(appointments)) { + throw new OverlappingAppointmentException(); + } + for (Appointment ap : appointments) { + if (this.overlaps(ap)) { + throw new OverlappingAppointmentException(); + } + } + internalList.addAll(appointments); + } + + public boolean isEmpty() { + return internalList.isEmpty(); + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } +} diff --git a/src/main/java/seedu/address/model/appointment/exceptions/AppointmentNotFoundException.java b/src/main/java/seedu/address/model/appointment/exceptions/AppointmentNotFoundException.java new file mode 100644 index 00000000000..7181e7c5171 --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/exceptions/AppointmentNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.appointment.exceptions; + +/** + * Signals that the operation will result in overlapping appointment intervals. + */ +public class AppointmentNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/appointment/exceptions/OverlappingAppointmentException.java b/src/main/java/seedu/address/model/appointment/exceptions/OverlappingAppointmentException.java new file mode 100644 index 00000000000..1f527830d66 --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/exceptions/OverlappingAppointmentException.java @@ -0,0 +1,12 @@ +package seedu.address.model.appointment.exceptions; + +/** + * Signals that the operation is unable to find the specified appointment. + */ +public class OverlappingAppointmentException extends RuntimeException { + public OverlappingAppointmentException() { + super("Operation would result in overlapping appointments"); + } +} + + diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 469a2cc9a1e..5d67806c11e 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -30,6 +30,10 @@ public Address(String address) { value = address; } + protected Address() { + value = null; + } + /** * Returns true if a given string is a valid email. */ @@ -53,6 +57,10 @@ public boolean equals(Object other) { return false; } + if (other instanceof EmptyAddress) { + return false; + } + Address otherAddress = (Address) other; return value.equals(otherAddress.value); } @@ -62,4 +70,7 @@ public int hashCode() { return value.hashCode(); } + public boolean isEmpty() { + return false; + } } diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index c62e512bc29..b7e90cacb94 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -43,6 +43,9 @@ public Email(String email) { checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); value = email; } + protected Email() { + value = null; + } /** * Returns if a given string is a valid email. @@ -67,6 +70,10 @@ public boolean equals(Object other) { return false; } + if (other instanceof EmptyEmail) { + return false; + } + Email otherEmail = (Email) other; return value.equals(otherEmail.value); } @@ -76,4 +83,7 @@ public int hashCode() { return value.hashCode(); } + public boolean isEmpty() { + return false; + } } diff --git a/src/main/java/seedu/address/model/person/EmptyAddress.java b/src/main/java/seedu/address/model/person/EmptyAddress.java new file mode 100644 index 00000000000..66328e448a5 --- /dev/null +++ b/src/main/java/seedu/address/model/person/EmptyAddress.java @@ -0,0 +1,28 @@ +package seedu.address.model.person; + +/** + * An Empty Address class, for use when no field is listed in a person's address during creation + */ +public class EmptyAddress extends Address { + public EmptyAddress() { + super(); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (other instanceof EmptyAddress) { + return true; + } + + return false; + } +} diff --git a/src/main/java/seedu/address/model/person/EmptyEmail.java b/src/main/java/seedu/address/model/person/EmptyEmail.java new file mode 100644 index 00000000000..48cffbd8141 --- /dev/null +++ b/src/main/java/seedu/address/model/person/EmptyEmail.java @@ -0,0 +1,28 @@ +package seedu.address.model.person; + +/** + * An Empty Email class, for use when no field is listed in a person's email during creation + */ +public class EmptyEmail extends Email { + public EmptyEmail() { + super(); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (other instanceof EmptyEmail) { + return true; + } + + return false; + } +} diff --git a/src/main/java/seedu/address/model/person/EmptyLevel.java b/src/main/java/seedu/address/model/person/EmptyLevel.java new file mode 100644 index 00000000000..28659cbdcfc --- /dev/null +++ b/src/main/java/seedu/address/model/person/EmptyLevel.java @@ -0,0 +1,33 @@ +package seedu.address.model.person; + +/** + * An Empty Level class, for use when no field is listed in a person's level during creation + */ +public class EmptyLevel extends Level { + public EmptyLevel() { + super(); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public String toString() { + return null; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (other instanceof EmptyLevel) { + return true; + } + + return false; + } +} diff --git a/src/main/java/seedu/address/model/person/EmptyNote.java b/src/main/java/seedu/address/model/person/EmptyNote.java new file mode 100644 index 00000000000..11ef44909fc --- /dev/null +++ b/src/main/java/seedu/address/model/person/EmptyNote.java @@ -0,0 +1,28 @@ +package seedu.address.model.person; + +/** + * An Empty Note class, for use when no field is listed in a person's note during creation + */ +public class EmptyNote extends Note { + public EmptyNote() { + super(); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (other instanceof EmptyNote) { + return true; + } + + return false; + } +} diff --git a/src/main/java/seedu/address/model/person/EmptyPhone.java b/src/main/java/seedu/address/model/person/EmptyPhone.java new file mode 100644 index 00000000000..9ecd7779fc7 --- /dev/null +++ b/src/main/java/seedu/address/model/person/EmptyPhone.java @@ -0,0 +1,28 @@ +package seedu.address.model.person; + +/** + * An Empty Phone class, for use when no field is listed in a person's phone during creation + */ +public class EmptyPhone extends Phone { + public EmptyPhone() { + super(); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (other instanceof EmptyPhone) { + return true; + } + + return false; + } +} diff --git a/src/main/java/seedu/address/model/person/Level.java b/src/main/java/seedu/address/model/person/Level.java new file mode 100644 index 00000000000..45f336c4d15 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Level.java @@ -0,0 +1,65 @@ +package seedu.address.model.person; + +/** + * Represents a Person's level in the address book. + * Guarantees: immutable; is always valid + */ +public class Level { + public static final String MESSAGE_CONSTRAINTS = "Levels should only be P1, P2, P3, P4, P5 or P6"; + + private final LevelEnum internalLevel; + + /** + * Constructs a {@code Level}. + * + * @param level A valid level. + */ + public Level(String level) { + level = level.trim().toUpperCase(); + this.internalLevel = LevelEnum.valueOf(level); + } + + public Level() { + this.internalLevel = null; + } + + @Override + public String toString() { + return internalLevel.toString(); + } + + /** + * Returns if a given string is a valid level. + */ + public static boolean isValidLevel(String test) { + try { + test = test.trim().toUpperCase(); + LevelEnum.valueOf(test); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + public boolean isEmpty() { + return false; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Level)) { + return false; + } + + if (other instanceof EmptyLevel) { + return false; + } + + Level otherLevel = (Level) other; + return internalLevel.equals(otherLevel.internalLevel); + } +} diff --git a/src/main/java/seedu/address/model/person/LevelEnum.java b/src/main/java/seedu/address/model/person/LevelEnum.java new file mode 100644 index 00000000000..1deed3918fb --- /dev/null +++ b/src/main/java/seedu/address/model/person/LevelEnum.java @@ -0,0 +1,5 @@ +package seedu.address.model.person; + +enum LevelEnum { + P1, P2, P3, P4, P5, P6 +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..20d52c81b9d 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -56,7 +56,9 @@ public boolean equals(Object other) { } Name otherName = (Name) other; - return fullName.equals(otherName.fullName); + String fullNameLower = fullName.toLowerCase(); + String otherNameLower = otherName.fullName.toLowerCase(); + return fullNameLower.equals(otherNameLower); } @Override diff --git a/src/main/java/seedu/address/model/person/Note.java b/src/main/java/seedu/address/model/person/Note.java new file mode 100644 index 00000000000..5b1e84c10ce --- /dev/null +++ b/src/main/java/seedu/address/model/person/Note.java @@ -0,0 +1,58 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +/** + * Represents a Person's note in the address book. + * Guarantees: immutable; is always valid + */ +public class Note { + public final String value; + + /** + * Constructs an {@code Note}. + * + * @param note A valid note. + */ + public Note(String note) { + requireNonNull(note); + value = note; + } + + protected Note() { + value = null; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Note)) { + return false; + } + + if (other instanceof EmptyNote) { + return false; + } + + Note otherNote = (Note) other; + return value.equals(otherNote.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + public boolean isEmpty() { + return false; + } +} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..95bc96a37ad 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -2,12 +2,17 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; +import seedu.address.commons.util.StringUtil; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.appointment.AppointmentList; import seedu.address.model.tag.Tag; /** @@ -20,21 +25,32 @@ public class Person { private final Name name; private final Phone phone; private final Email email; + private final Note note; // Data fields private final Address address; private final Set tags = new HashSet<>(); + private final AppointmentList appointments; + private final Set subjects = new HashSet<>(); + private final Level level; /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person( + Name name, Phone phone, Email email, Address address, Note note, + Set tags, AppointmentList appointments, Set subjects, Level level + ) { + requireAllNonNull(name, phone, email, address, tags, appointments, subjects, level); this.name = name; this.phone = phone; this.email = email; this.address = address; + this.note = note; this.tags.addAll(tags); + this.appointments = appointments; + this.subjects.addAll(subjects); + this.level = level; } public Name getName() { @@ -53,6 +69,14 @@ public Address getAddress() { return address; } + public Note getNote() { + return note; + } + + public Level getLevel() { + return level; + } + /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -61,6 +85,84 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + public AppointmentList getAppointments() { + return appointments; + } + + /** + * Returns an immutable subject set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getSubjects() { + return Collections.unmodifiableSet(subjects); + } + + /** + * Returns a formatted string that contains all the details of the Person object for the + * View command. + */ + public List getViewDetails() { + List detailList = new ArrayList<>(); + + assert this.getName() != null; + detailList.add(this.getName().fullName.toUpperCase() + "\n"); + detailList.add(getSummary()); + detailList.add(StringUtil.SEPARATOR); + + detailList.add("\nDETAILS:\n"); + detailList.add(this.getPhone().isEmpty() ? "-" : this.getPhone().value + "\n"); + detailList.add(this.getEmail().isEmpty() ? "-" : this.getEmail().value + "\n"); + detailList.add(this.getAddress().isEmpty() ? "-" : this.getAddress().value + "\n"); + detailList.add(StringUtil.SEPARATOR); + + detailList.add("\nAPPOINTMENTS:\n"); + detailList.add(this.getAppointments().isEmpty() + ? "-\n" + : this.getAppointments() + .asUnmodifiableObservableList() + .stream() + .map(Object::toString) + .map(str -> str + "\n") + .collect(Collectors.joining())); + detailList.add(StringUtil.SEPARATOR); + + detailList.add("\nNOTES:\n" + (this.getNote().isEmpty() ? "-" : this.getNote().value)); + + return detailList; + } + + /** + * Returns a string containing the level, subject and tags of the person. + */ + public String getSummary() { + String summaryString = "\n"; + summaryString += this.getLevel().isEmpty() + ? "" + : "[" + this.getLevel().toString() + "] "; + summaryString += this.getSubjects().isEmpty() + ? "" + : this.getSubjects().stream() + .map(Object::toString) + .map(str -> str + " ") + .collect(Collectors.joining()); + summaryString += this.getTags().isEmpty() + ? "\n" + : this.getTags().stream() + .map(Object::toString) + .map(str -> str + " ") + .collect(Collectors.joining()) + .trim() + "\n"; + + return summaryString; + } + + /** + * Returns a boolean value which indicates whether the person has any appointments. + */ + public boolean hasAppointments() { + return !appointments.isEmpty(); + } + /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. @@ -94,24 +196,42 @@ public boolean equals(Object other) { && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) - && tags.equals(otherPerson.tags); + && note.equals(otherPerson.note) + && tags.equals(otherPerson.tags) + && appointments.equals(otherPerson.appointments) + && subjects.equals(otherPerson.subjects) + && level.equals(otherPerson.level); } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, address, tags, appointments); } @Override public String toString() { - return new ToStringBuilder(this) - .add("name", name) - .add("phone", phone) - .add("email", email) - .add("address", address) - .add("tags", tags) - .toString(); - } + ToStringBuilder returnedString = new ToStringBuilder(this) + .add("name", name); + if (!phone.isEmpty()) { + returnedString.add("phone", phone); + } + if (!email.isEmpty()) { + returnedString.add("email", email); + } + if (!address.isEmpty()) { + returnedString.add("address", address); + } + if (!note.isEmpty()) { + returnedString.add("note", note); + } + returnedString.add("tags", tags); + returnedString.add("appointments", appointments); + returnedString.add("subjects", subjects); + if (!level.isEmpty()) { + returnedString.add("level", level); + } + return returnedString.toString(); + } } diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d733f63d739..ed28f370ca9 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -26,6 +26,10 @@ public Phone(String phone) { value = phone; } + protected Phone() { + value = null; + } + /** * Returns true if a given string is a valid phone number. */ @@ -49,6 +53,10 @@ public boolean equals(Object other) { return false; } + if (other instanceof EmptyPhone) { + return false; + } + Phone otherPhone = (Phone) other; return value.equals(otherPhone.value); } @@ -58,4 +66,7 @@ public int hashCode() { return value.hashCode(); } + public boolean isEmpty() { + return false; + } } diff --git a/src/main/java/seedu/address/model/person/Subject.java b/src/main/java/seedu/address/model/person/Subject.java new file mode 100644 index 00000000000..098e17d0100 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Subject.java @@ -0,0 +1,64 @@ +package seedu.address.model.person; + +/** + * Represents a Person's subject in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidSubject(String)} + */ +public class Subject { + + public static final String MESSAGE_CONSTRAINTS = "Subjects should only be ENGLISH, MATH, SCIENCE or MT."; + private final SubjectEnum internalSubject; + + /** + * Constructs a {@code Subject}. + * + * @param subject A valid subject. + */ + public Subject(String subject) { + subject = subject.trim().toUpperCase(); + this.internalSubject = SubjectEnum.valueOf(subject); + } + + /** + * Format state as text for viewing. + */ + public String toString() { + return '[' + internalSubject.toString() + ']'; + } + + public String getSubject() { + return internalSubject.toString(); + } + + /** + * Returns true if a given string is a valid subject. + */ + public static boolean isValidSubject(String test) { + try { + test = test.trim().toUpperCase(); + SubjectEnum.valueOf(test); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + @Override + public int hashCode() { + return internalSubject.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Subject)) { + return false; + } + + Subject otherSubject = (Subject) other; + return internalSubject.equals(otherSubject.internalSubject); + } +} diff --git a/src/main/java/seedu/address/model/person/SubjectEnum.java b/src/main/java/seedu/address/model/person/SubjectEnum.java new file mode 100644 index 00000000000..048706ac145 --- /dev/null +++ b/src/main/java/seedu/address/model/person/SubjectEnum.java @@ -0,0 +1,5 @@ +package seedu.address.model.person; + +enum SubjectEnum { + ENGLISH, MATH, SCIENCE, MT +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index cc0a68d79f9..bf930ccd469 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -3,8 +3,10 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.stream.Collectors; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -36,6 +38,41 @@ public boolean contains(Person toCheck) { return internalList.stream().anyMatch(toCheck::isSamePerson); } + /** + * Returns list of names similar to the given person's name. + */ + public List findNearDuplicates(Person toCheck) { + requireNonNull(toCheck); + String toCheckName = normalizeName(toCheck.getName().toString()); + + List nearDuplicates = new ArrayList<>(); + for (String name : getAllNames()) { + String normalizedCurrentName = normalizeName(name); + if (toCheckName.equals(normalizedCurrentName)) { + // Add the "original" duplicate name + nearDuplicates.add(name); + } + } + return nearDuplicates; + } + + /** + * Retrieves the names of all persons in the list. + */ + public List getAllNames() { + return internalList.stream() + .map(person -> person.getName().toString()) + .collect(Collectors.toList()); + } + + /** + * Normalizes names by removing unnecessary whitespaces and lowering the case. + */ + public String normalizeName(String name) { + requireNonNull(name); + return name.trim().replaceAll("\\s+", "").toLowerCase(); + } + /** * Adds a person to the list. * The person must not already exist in the list. diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..6b1bd04273a 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,42 +1,57 @@ package seedu.address.model.util; import java.util.Arrays; +import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.AppointmentList; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.Level; import seedu.address.model.person.Name; +import seedu.address.model.person.Note; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Subject; import seedu.address.model.tag.Tag; /** * Contains utility methods for populating {@code AddressBook} with sample data. */ public class SampleDataUtil { + + public static final Note EMPTY_NOTE = new Note(""); + public static Person[] getSamplePersons() { return new Person[] { new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), + new Address("Blk 30 Geylang Street 29, #06-40"), new Note("Weak in fractions."), + getTagSet("referral"), getAppointmentList("12:00-13:00 SUN", "00:00-01:00 MON"), + getSubjectSet("Math", "Science"), new Level("P4")), new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), EMPTY_NOTE, + getTagSet("weak"), getAppointmentList("08:00-09:00 MON"), + getSubjectSet("Math", "English"), new Level("P2")), new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), + new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), EMPTY_NOTE, + getTagSet("payment"), getAppointmentList("14:20-15:00 SAT"), + getSubjectSet("Science"), new Level("P3")), new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), EMPTY_NOTE, + new HashSet(), getAppointmentList("16:30-18:00 THU", "19:00-20:00 WED"), + getSubjectSet("Math", "Science"), new Level("P6")), new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), + new Address("Blk 47 Tampines Street 20, #17-35"), EMPTY_NOTE, + new HashSet(), getAppointmentList("16:00-17:00 TUE"), + getSubjectSet("Math", "Science"), new Level("P6")), new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + new Address("Blk 45 Aljunied Street 85, #11-31"), EMPTY_NOTE, + new HashSet(), getAppointmentList("14:30-15:00 WED"), + getSubjectSet("Math", "Science"), new Level("P4")) }; } @@ -57,4 +72,24 @@ public static Set getTagSet(String... strings) { .collect(Collectors.toSet()); } + /** + * Returns an appointment set containing the list of strings given. + */ + public static AppointmentList getAppointmentList(String... strings) { + AppointmentList appointments = new AppointmentList(); + appointments.addAll(Arrays.stream(strings) + .map(Appointment::new) + .collect(Collectors.toList())); + return appointments; + } + + /** + * Returns a subject set containing the list of strings given. + */ + public static Set getSubjectSet(String... strings) { + return Arrays.stream(strings) + .map(Subject::new) + .collect(Collectors.toSet()); + } + } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedAppointment.java b/src/main/java/seedu/address/storage/JsonAdaptedAppointment.java new file mode 100644 index 00000000000..a4cd7408c11 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedAppointment.java @@ -0,0 +1,49 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.tag.Tag; + +/** + * Jackson-friendly version of {@link Appointment}. + */ +class JsonAdaptedAppointment { + + private final String appointment; + + /** + * Constructs a {@code JsonAdaptedAddress} with the given {@code appointment}. + */ + @JsonCreator + public JsonAdaptedAppointment(String appointment) { + this.appointment = appointment; + } + + /** + * Converts a given {@code Appointment} into this class for Jackson use. + */ + public JsonAdaptedAppointment(Appointment source) { + appointment = source.value; + } + + @JsonValue + public String getAppointment() { + return appointment; + } + + /** + * Converts this Jackson-friendly adapted appointment object into the model's {@code Appointment} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted appointment. + */ + public Appointment toModelType() throws IllegalValueException { + if (!Appointment.isValidAppointment(appointment)) { + throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); + } + return new Appointment(appointment); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..17eaa351c71 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -10,11 +10,21 @@ import com.fasterxml.jackson.annotation.JsonProperty; import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.AppointmentList; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.EmptyAddress; +import seedu.address.model.person.EmptyEmail; +import seedu.address.model.person.EmptyLevel; +import seedu.address.model.person.EmptyNote; +import seedu.address.model.person.EmptyPhone; +import seedu.address.model.person.Level; import seedu.address.model.person.Name; +import seedu.address.model.person.Note; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Subject; import seedu.address.model.tag.Tag; /** @@ -28,7 +38,11 @@ class JsonAdaptedPerson { private final String phone; private final String email; private final String address; + private final String note; private final List tags = new ArrayList<>(); + private final List appointments = new ArrayList<>(); + private final List subjects = new ArrayList<>(); + private final String level; /** * Constructs a {@code JsonAdaptedPerson} with the given person details. @@ -36,14 +50,24 @@ class JsonAdaptedPerson { @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("note") String note, @JsonProperty("tags") List tags, + @JsonProperty("appointments") List appointments, + @JsonProperty("subjects") List subjects, @JsonProperty("level") String level) { this.name = name; this.phone = phone; this.email = email; this.address = address; + this.note = note; + this.level = level; if (tags != null) { this.tags.addAll(tags); } + if (appointments != null) { + this.appointments.addAll(appointments); + } + if (subjects != null) { + this.subjects.addAll(subjects); + } } /** @@ -54,9 +78,17 @@ public JsonAdaptedPerson(Person source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; + note = source.getNote().value; + level = source.getLevel().toString(); tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + appointments.addAll(source.getAppointments().asUnmodifiableObservableList().stream() + .map(JsonAdaptedAppointment::new) + .collect(Collectors.toList())); + subjects.addAll(source.getSubjects().stream() + .map(JsonAdaptedSubject::new) + .collect(Collectors.toList())); } /** @@ -66,9 +98,17 @@ public JsonAdaptedPerson(Person source) { */ public Person toModelType() throws IllegalValueException { final List personTags = new ArrayList<>(); + final List personAppointments = new ArrayList<>(); + final List personSubjects = new ArrayList<>(); for (JsonAdaptedTag tag : tags) { personTags.add(tag.toModelType()); } + for (JsonAdaptedAppointment appointment : appointments) { + personAppointments.add(appointment.toModelType()); + } + for (JsonAdaptedSubject subject : subjects) { + personSubjects.add(subject.toModelType()); + } if (name == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); @@ -78,32 +118,61 @@ public Person toModelType() throws IllegalValueException { } final Name modelName = new Name(name); + Phone usedPhone; if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { + usedPhone = new EmptyPhone(); + } else if (!Phone.isValidPhone(phone)) { throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); + } else { + usedPhone = new Phone(phone); } - final Phone modelPhone = new Phone(phone); + final Phone modelPhone = usedPhone; + Email usedEmail; if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { + usedEmail = new EmptyEmail(); + } else if (!Email.isValidEmail(email)) { throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); + } else { + usedEmail = new Email(email); } - final Email modelEmail = new Email(email); + final Email modelEmail = usedEmail; + Address usedAddress; if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { + usedAddress = new EmptyAddress(); + } else if (!Address.isValidAddress(address)) { throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); + } else { + usedAddress = new Address(address); + } + final Address modelAddress = usedAddress; + + Note usedNote; + if (note == null) { + usedNote = new EmptyNote(); + } else { + usedNote = new Note(note); + } + final Note modelNote = usedNote; + + Level usedLevel; + if (level == null) { + usedLevel = new EmptyLevel(); + } else { + usedLevel = new Level(level); } - final Address modelAddress = new Address(address); + final Level modelLevel = usedLevel; final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + + final AppointmentList modelAppointments = new AppointmentList(); + modelAppointments.addAll(personAppointments); + + final Set modelSubjects = new HashSet<>(personSubjects); + + return new Person(modelName, modelPhone, modelEmail, modelAddress, + modelNote, modelTags, modelAppointments, modelSubjects, modelLevel); } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedSubject.java b/src/main/java/seedu/address/storage/JsonAdaptedSubject.java new file mode 100644 index 00000000000..b974335b118 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedSubject.java @@ -0,0 +1,49 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Subject; +import seedu.address.model.tag.Tag; + + +/** + * Jackson-friendly version of {@link seedu.address.model.person.Subject}. + */ +public class JsonAdaptedSubject { + + private final String subject; + + /** + * Constructs a {@code JsonAdaptedSubject} with the given {@code subject}. + */ + @JsonCreator + public JsonAdaptedSubject(String subject) { + this.subject = subject; + } + + /** + * Converts a given {@code Subject} into this class for Jackson use. + */ + public JsonAdaptedSubject(Subject source) { + subject = source.getSubject(); + } + + @JsonValue + public String getSubject() { + return subject; + } + + /** + * Converts this Jackson-friendly adapted subject object into the model's {@code Subject} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted subject. + */ + public Subject toModelType() throws IllegalValueException { + if (!Subject.isValidSubject(subject)) { + throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); + } + return new Subject(subject); + } +} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..8114d35d977 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -11,6 +11,8 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.AppointmentList; import seedu.address.model.person.Person; /** @@ -20,15 +22,21 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_OVERLAPPING_APPOINTMENT = + "Appointment list contains overlapping appointment(s)."; + public static final String MESSAGE_APPOINTMENTS_PERSONS_MISMATCH = "Persons list and appointments list don't match"; private final List persons = new ArrayList<>(); + private final List appointments = new ArrayList<>(); /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableAddressBook} with the given persons and appointments. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { + public JsonSerializableAddressBook(@JsonProperty("persons") List persons, + @JsonProperty("appointments") List appointments) { this.persons.addAll(persons); + this.appointments.addAll(appointments); } /** @@ -37,7 +45,14 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2324s2-cs2103-f09-3.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..484200116cd 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -34,7 +34,6 @@ public class MainWindow extends UiPart { private PersonListPanel personListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; - @FXML private StackPane commandBoxPlaceholder; diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..79c6b5ca03d 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -8,6 +8,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import seedu.address.model.person.Person; +import seedu.address.model.person.Subject; /** * An UI component that displays information of a {@code Person}. @@ -35,11 +36,14 @@ public class PersonCard extends UiPart { @FXML private Label phone; @FXML - private Label address; - @FXML private Label email; @FXML - private FlowPane tags; + private FlowPane summary; + @FXML + private FlowPane subjects; + @FXML + private FlowPane level; + /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. @@ -49,11 +53,32 @@ public PersonCard(Person person, int displayedIndex) { this.person = person; id.setText(displayedIndex + ". "); name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); + phone.setText(person.getPhone().isEmpty() ? "-" : person.getPhone().value); + email.setText(person.getEmail().isEmpty() ? "-" : person.getEmail().value); + + String level = person.getLevel().toString(); + if (level != null) { + summary.getChildren().add(createSummaryLabel(level, level)); + } + + person.getSubjects().stream() + .sorted(Comparator.comparing(Subject::getSubject)) + .forEach(subject -> summary.getChildren() + .add(createSummaryLabel(subject.getSubject(), subject.getSubject()))); + person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + .forEach(tag -> summary.getChildren().add(createSummaryLabel(tag.tagName, "tag"))); + + } + + /** + * Returns a Label object that is used in the summary FlowPane containing the specified text content + * and style class. + */ + private Label createSummaryLabel(String content, String classToAdd) { + Label customLabel = new Label(content); + customLabel.getStyleClass().add(classToAdd); + return customLabel; } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..6b6a03b9643 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/tutorrec.png"; private Logic logic; private MainWindow mainWindow; diff --git a/src/main/resources/images/tutorrec.png b/src/main/resources/images/tutorrec.png new file mode 100644 index 00000000000..ea9cb1a088f Binary files /dev/null and b/src/main/resources/images/tutorrec.png differ diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..2543f77aff3 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,5 +1,5 @@ .background { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #000000; background-color: #383838; /* Used in the default.html file */ } @@ -91,6 +91,8 @@ -fx-background-insets: 0; -fx-padding: 0; -fx-background-color: derive(#1d1d1d, 20%); + -fx-border-color: #A4A4A4; + -fx-border-width: 2px; } .list-cell { @@ -100,11 +102,11 @@ } .list-cell:filled:even { - -fx-background-color: #3c3e3f; + -fx-background-color: #4D4D4D; } .list-cell:filled:odd { - -fx-background-color: #515658; + -fx-background-color: #2E2E2E; } .list-cell:filled:selected { @@ -133,17 +135,15 @@ } .stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #000000; } .pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); - -fx-border-top-width: 1px; + -fx-background-color: #000000; } .status-bar { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: #565656; } .result-display { @@ -159,7 +159,7 @@ .status-bar .label { -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: #A3A3A3; -fx-padding: 4px; -fx-pref-height: 30px; } @@ -181,11 +181,11 @@ } .grid-pane .stack-pane { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: #000000; } .context-menu { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: #000000; } .context-menu .label { @@ -193,7 +193,10 @@ } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #000000; + -fx-border-color: #ffffff; + -fx-border-width: 1px; + -fx-border-style: hidden hidden solid hidden; } .menu-bar .label { @@ -318,11 +321,11 @@ } #commandTextField { - -fx-background-color: transparent #383838 transparent #383838; + -fx-background-color: #2E2E2E; -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; + -fx-border-color: #ffffff; -fx-border-insets: 0; - -fx-border-width: 1; + -fx-border-width: 1.5; -fx-font-family: "Segoe UI Light"; -fx-font-size: 13pt; -fx-text-fill: white; @@ -332,21 +335,75 @@ -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); } +#personListPanel { + -fx-border-color: #A4A4A4; + -fx-border-width: 2px; +} + #resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; + -fx-background-color: #2E2E2E; -fx-background-radius: 0; + -fx-border-color: #555353; + -fx-border-insets: 0; + -fx-border-width: 2; } -#tags { - -fx-hgap: 7; +#summary { + -fx-hgap: 5; -fx-vgap: 3; + -fx-padding: 2 0 2 0; } -#tags .label { +#summary .label { -fx-text-fill: white; -fx-background-color: #3e7b91; - -fx-padding: 1 3 1 3; - -fx-border-radius: 2; - -fx-background-radius: 2; - -fx-font-size: 11; + -fx-padding: 2 8 2 8; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-font-size: 13; + -fx-font-weight: bold; +} + +#summary .tag { + -fx-background-color: #3e7b91; +} + +#summary .P1 { + -fx-background-color: #9340f7; +} + +#summary .P2 { + -fx-background-color: #bd40f7; +} + +#summary .P3 { + -fx-background-color: #e540f7; +} + +#summary .P4 { + -fx-background-color: #f740c7; +} + +#summary .P5 { + -fx-background-color: #f74083; +} + +#summary .P6 { + -fx-background-color: #f74040; +} + +#summary .ENGLISH { + -fx-background-color: #4c49fc; +} + +#summary .MATH { + -fx-background-color: #94622c; +} + +#summary .SCIENCE { + -fx-background-color: #298028; +} + +#summary .MT { + -fx-background-color: #808028; } diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..520a0f2670b 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -6,15 +6,15 @@ - + + - + - + @@ -23,7 +23,7 @@ - + @@ -32,27 +32,27 @@ + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f5e812e25e6..1f9234ea77e 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -14,7 +14,7 @@ - + @@ -27,9 +27,8 @@ diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 01b691792a9..6044fe878ee 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -5,5 +5,5 @@ -