diff --git a/README.md b/README.md index 16208adb9b6..cdc28ae9081 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +
+ +# Tutorly + +![Java CI](https://img.shields.io/github/actions/workflow/status/AY2425S2-CS2103T-T17-3/tp/gradle.yml?style=for-the-badge&label=Java%20CI) +![Codecov](https://img.shields.io/codecov/c/gh/AY2425S2-CS2103T-T17-3/tp?style=for-the-badge) + +
![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/#contributing-to-se-edu) for more info. +## About + +Tutorly is a desktop application designed specifically for private tutors. It empowers tutors to quickly track student records, log lesson details, record attendance, and generate progress reports - all through fast, keyboard-driven interactions. + +For more information, please refer to our [Product Website](https://ay2425s2-cs2103t-t17-3.github.io/tp/). + +## Acknowledgements +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). diff --git a/build.gradle b/build.gradle index 0db3743584e..3a97e8824a1 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id 'jacoco' } -mainClassName = 'seedu.address.Main' +mainClassName = 'tutorly.Main' sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -66,7 +66,12 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'tutorly.jar' +} + +run { + standardInput = System.in + enableAssertions = true } defaultTasks 'clean', 'test' diff --git a/copyright.txt b/copyright.txt index 93aa2a39ce2..cf851c173e4 100644 --- a/copyright.txt +++ b/copyright.txt @@ -4,6 +4,9 @@ Copyright by Susumu Yoshida - http://www.mcdodesign.com/ - address_book_32.png - AddressApp.ico -Copyright by Jan Jan Kovařík - http://glyphicons.com/ +Copyright by Emojipedia - https://emojipedia.org/ - calendar.png -- edit.png +- email.png +- house.png +- memo.png +- telephone.png diff --git a/docs/AboutUs.md b/docs/AboutUs.md index ff3f04abd02..d0884c1bbce 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,52 @@ title: About Us We are a team based in the [School of Computing, National University of Singapore](https://www.comp.nus.edu.sg). -You can reach us at the email `seer[at]comp.nus.edu.sg` +You can reach us at the email `tutorly[at]groups.nus.edu.sg` ## Project team -### John Doe +### Nickie Tan - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/nickt121)] -* Role: Project Advisor +* Role: Developer +* Responsibilities: Documentation, Testing -### Jane Doe +### Roy Tay - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/themintchoco)] * Role: Team Lead -* Responsibilities: UI +* Responsibilities: Git expert, Integration -### Johnny Doe +### Chen Jiahao - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](https://github.com/Neilchen863)] -* Role: Developer -* Responsibilities: Data +* Role: developer +* Responsibilities: Testing, Documentation -### Jean Doe +### Lam Yu Han Bryan - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/sociallyineptweeb)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Deliverables and deadlines, Scheduling and tracking -### James Doe +### Lee Zhan Hong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/Zhannyhong)] * Role: Developer -* Responsibilities: UI +* Responsibilities: IntelliJ expert, Code quality + diff --git a/docs/DevOps.md b/docs/DevOps.md index d2fd91a6001..636092c9e46 100644 --- a/docs/DevOps.md +++ b/docs/DevOps.md @@ -73,7 +73,7 @@ Any warnings or errors will be printed out to the console. Here are the steps to create a new release. -1. Update the version number in [`MainApp.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java). +1. Update the version number in [`MainApp.java`](https://github.com/AY2425S2-CS2103T-T17-3/tp/tree/master/src/main/java/tutorly/MainApp.java). 1. Generate a fat JAR file using Gradle (i.e., `gradlew shadowJar`). 1. Tag the repo with the version number. e.g. `v0.1` 1. [Create a new release using GitHub](https://help.github.com/articles/creating-releases/). Upload the JAR file you created. diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 743c65a49d2..7d23fe15de8 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -9,7 +9,8 @@ title: Developer Guide ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* This project is based on the [AddressBook-Level3](https://github.com/se-edu/addressbook-level3) project created by the SE-EDU initiative. +* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) -------------------------------------------------------------------------------------------------------------------- @@ -36,7 +37,7 @@ Given below is a quick overview of main components and how they interact with ea **Main components of the architecture** -**`Main`** (consisting of classes [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. +**`Main`** (consisting of classes [`Main`](https://github.com/AY2425S2-CS2103T-T17-3/tp/tree/master/src/main/java/tutorly/Main.java) and [`MainApp`](https://github.com/AY2425S2-CS2103T-T17-3/tp/tree/master/src/main/java/tutorly/MainApp.java)) is in charge of the app launch and shut down. * At app launch, it initializes the other components in the correct sequence, and connects them up with each other. * At shut down, it shuts down the other components and invokes cleanup methods where necessary. @@ -51,7 +52,7 @@ The bulk of the app's work is done by the following four components: **How the architecture components interact with each other** -The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `student delete 1`. @@ -68,24 +69,24 @@ The sections below give more details of each component. ### UI component -The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) +The **API** of this component is specified in [`Ui.java`](https://github.com/AY2425S2-CS2103T-T17-3/tp/tree/master/src/main/java/tutorly/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. The classes `PersonListPanel`, `SessionListPanel`, and `AttendanceRecordListPanel` inherit from the abstract `ListPanel` class which captures the commonalities of a panel in the GUI that displays a list of items. Each item in the list is represented as a card (e.g. `SessionCard`). All these, including the `MainWindow` and `ListPanel`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/AY2425S2-CS2103T-T17-3/tp/tree/master/src/main/java/tutorly/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2425S2-CS2103T-T17-3/tp/tree/master/src/main/resources/view/MainWindow.fxml) The `UI` component, * executes user commands using the `Logic` component. * listens for changes to `Model` data so that the UI can be updated with the modified data. * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +* depends on some classes in the `Model` component, as it displays `Person`, `Session`, and `AttendanceRecord` objects residing in the `Model`. ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2425S2-CS2103T-T17-3/tp/tree/master/src/main/java/tutorly/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: @@ -95,13 +96,13 @@ The sequence diagram below illustrates the interactions within the `Logic` compo ![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) -
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram. +
:information_source: **Note:** The lifeline for `StudentCommandParser` and `DeleteStudentCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram.
How the `Logic` component works: -1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. +1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `DeleteStudentCommandParser`) and uses it to parse the command. +1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteStudentCommand`) which is executed by the `LogicManager`. 1. The command can communicate with the `Model` when it is executed (e.g. to delete a person).
Note that although this is shown as a single step in the diagram above (for simplicity), in the code it can take several interactions (between the command object and the `Model`) to achieve. 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. @@ -111,32 +112,25 @@ Here are the other classes in `Logic` (omitted from the class diagram above) tha How the parsing works: -* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. -* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. +* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `StudentCommandParser`) which can then create more parsers as required (e.g., `AddStudentCommandParser`) and use the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddStudentCommand`) which the `AddressBookParser` returns back as a `Command` object. +* All `XYZCommandParser` classes (e.g., `StudentCommandParser`, `AddStudentCommandParser`, `DeleteStudentCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) +**API** : [`Model.java`](https://github.com/AY2425S2-CS2103T-T17-3/tp/tree/master/src/main/java/tutorly/model/Model.java) The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the address book data i.e., all `Person`, `Session`, and `AttendanceRecord` objects (which are contained in `UniquePersonList`, `UniqueSessionList` and `UniqueAttendanceRecordList` objects respectively), as well as the IDs of the next `Person` or `Session` to be added. +* stores the currently filtered `Person` and `Session` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` and `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 -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](https://github.com/AY2425S2-CS2103T-T17-3/tp/tree/master/src/main/java/tutorly/storage/Storage.java) @@ -147,7 +141,7 @@ The `Storage` component, ### Common classes -Classes used by multiple components are in the `seedu.address.commons` package. +Classes used by multiple components are in the `tutorly.commons` package. -------------------------------------------------------------------------------------------------------------------- @@ -155,228 +149,398 @@ Classes used by multiple components are in the `seedu.address.commons` package. This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature - -#### Proposed Implementation - -The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: - -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. - -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. - -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. - -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +### Undo feature -![UndoRedoState0](images/UndoRedoState0.png) +There are several ways to build an undo feature. One way is to keep a stack of `AddressBook`s in memory. Each time a change is made to the `AddressBook`, push a copy of the current `AddressBook` onto the stack. When the user requests an undo, pop the top `AddressBook` from the stack and set it as the current `AddressBook`. This is a straightforward and relatively less error-prone way to implement undo. However, it has the following drawbacks: +* It requires a lot of memory to store multiple copies of the `AddressBook` object. +* It is not efficient to copy the entire `AddressBook` object every time a change is made. -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +#### Our implementation -![UndoRedoState1](images/UndoRedoState1.png) +We build upon this idea, but instead of keeping a stack of `AddressBook`s, we keep a stack of `Command`s. Each time a change is made to the `AddressBook`, we push the reverse of the command that made the change onto the stack. When the user requests an undo, we pop the top `Command` from the stack and execute it. This way, we do not need to keep multiple copies of the `AddressBook` object in memory. This is more efficient in terms of memory usage and performance. -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +Each `Command` defines its reverse operation during execution. When building the `CommandResult`, the `Command` also specifies the reverse command to be executed when the user requests an undo. This is kept track of by `LogicManager`, which maintains a stack of `Command`s. When the undo command is executed, `LogicManager` pops the top `Command` from the stack and executes it. The `Command` then executes its reverse operation on the `Model` to revert the changes made by the original command. -![UndoRedoState2](images/UndoRedoState2.png) +The following sequence diagram shows how an undo operation goes through the `Logic` component, when used to undo a student addition. -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +![Interactions Inside the Logic Component for the `undo` Command](images/UndoSequenceDiagram.png) +
:information_source: **Note:** The lifeline for `AddStudentCommand`, `UndoCommand`, and `DeleteStudentCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram.
-Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. - -![UndoRedoState3](images/UndoRedoState3.png) - -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather -than attempting to perform the undo. - -
- -The following sequence diagram shows how an undo operation goes through the `Logic` component: +-------------------------------------------------------------------------------------------------------------------- -![UndoSequenceDiagram](images/UndoSequenceDiagram-Logic.png) +## **Documentation, logging, testing, configuration, dev-ops** -
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +* [Documentation guide](Documentation.md) +* [Testing guide](Testing.md) +* [Logging guide](Logging.md) +* [Configuration guide](Configuration.md) +* [DevOps guide](DevOps.md) -
+-------------------------------------------------------------------------------------------------------------------- -Similarly, how an undo operation goes through the `Model` component is shown below: +## **Appendix: Requirements** -![UndoSequenceDiagram](images/UndoSequenceDiagram-Model.png) +### Product scope -The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +**Target user profile**: -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +* private tutor +* need to track large number of students' details and sessions +* prefer desktop apps over other types +* can type fast +* prefers typing to mouse interactions +* is reasonably comfortable using CLI apps -
+**Value proposition**: tracks and manages students’ details faster than a typical mouse/GUI driven app, +reducing manual effort and ensuring better organization. -Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. -![UndoRedoState4](images/UndoRedoState4.png) +### User stories -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. +Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -![UndoRedoState5](images/UndoRedoState5.png) +| 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 student with basic details | begin tracking their progress | +| `* * *` | user | record additional student details | recall other information about the student | +| `* * *` | user with many students | search a student by name or phone number | quickly find their details before a session without going through the entire list | +| `* * *` | user | edit student records | update student details when they change | +| `* * *` | long-time user | delete old student records | stop tracking students that I no longer teach | +| `* * *` | user | record student attendance | track my student's participation | +| `* *` | potential user | see the app populated with sample data initially | easily visualise how it will look like in real use | +| `* *` | user ready to start using the app | delete all sample data | start fresh with my actual students | +| `* *` | user | log lesson feedback | keep track of progress of a student in the session | +| `* *` | user | create custom tags for students | categorise them based on needs | +| `*` | user with many students | sort students by any field | locate a student easily | +| `*` | user | filter students by custom tags | see all students with particular needs | +| `*` | expert user | bulk-edit lesson notes or assignments | save time by updating multiple records at once | +| `*` | user teaching multiple subjects | customise tracking fields for different subjects | tailor my records to different teaching needs | +| `*` | user teaching group classes | create group sessions with multiple students | track their progress collectively as a class | +| `*` | user | generate a progress report for a student | share updates with parents | +| `*` | user | receive a weekly summary of my sessions | review my workload | +| `*` | user | set reminder for upcoming sessions | remember upcoming lessons | +| `*` | user | view reminders for upcoming sessions | plan my schedule | +| `*` | user | hide private contact details | minimise chance of someone else seeing them by accident | + + +### Use Cases + +(For all use cases below, the **System** is `Tutorly`, and the **Actor** is the `tutor`, unless specified otherwise.) -The following activity diagram summarizes what happens when a user executes a new command: +--- - +**Use case: Add a student record** -#### Design considerations: +**MSS** -**Aspect: How undo & redo executes:** +1. Tutor requests to add a new student with the required details (Name, Phone, Email, Address, Tag, Memo). +2. Tutorly validates the input. +3. Tutorly adds the student profile to the database and confirms success. -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. + Use case ends. -* **Alternative 2:** Individual command knows how to undo/redo by - itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. +**Extensions** -_{more aspects and alternatives to be added}_ +- 1a. Tutor does not provide all compulsory fields. + - 1a1. Tutorly prompts for the missing information. + - 1a2. Tutor enters the all the required details + - Use case resumes at step 2. -### \[Proposed\] Data archiving +- 2a. Tutor provides invalid input for any field. + - 2a1. Tutorly displays an appropriate error message. + - 2a2. Tutor corrects the input. + - Use case resumes at step 2. -_{Explain here how the data archiving feature will be implemented}_ +- 2b. The student already exists (Same Name). + - 2b1. Tutorly displays an error message indicating the student already exists. + - Use case ends. +--- --------------------------------------------------------------------------------------------------------------------- +**Use case: Search for a student record** -## **Documentation, logging, testing, configuration, dev-ops** +**MSS** -* [Documentation guide](Documentation.md) -* [Testing guide](Testing.md) -* [Logging guide](Logging.md) -* [Configuration guide](Configuration.md) -* [DevOps guide](DevOps.md) +1. Tutor requests to search for a student by entering a query. +2. Tutorly validates the search query. +3. Tutorly retrieves and displays matching student profiles. --------------------------------------------------------------------------------------------------------------------- + Use case ends. -## **Appendix: Requirements** +**Extensions** -### Product scope +- 3a. No students match the search query. + - 3a1. Tutorly responds that no students match the search query. + - Use case ends. -**Target user profile**: +--- -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +**Use case: Update a student record** -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**MSS** +1. Tutor requests to update a student record by providing the student’s Identifier and updated details. (e.g., Name, Phone, Email, Address, Tag, Memo). +2. Tutorly validates the input. +3. Tutorly updates the student profile and confirms success. -### User stories + Use case ends. -Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` +**Extensions** -| 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 | +- 1a. Tutor does not provide any update parameters. + - 1a1. Tutorly displays an error message indicating that there must be at least one update parameter. + - Use case ends. -*{More to be added}* +- 2a. The student Identifier does not exist. + - 2a1. Tutorly responds that the student does not exist. + - Use case ends. -### Use cases +- 2c. Tutor provides invalid input for any field. + - 2c1. Tutorly displays an appropriate error message. + - 2c2. Tutor corrects the input. + - Use case resumes at step 2. -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +--- -**Use case: Delete a person** +**Use case: Delete a student record** **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. Tutor requests to delete a student record by providing the student’s Identifier. +2. Tutorly validates the request and performs the action. +3. Tutorly confirms the success of the operation. - Use case ends. + Use case ends. **Extensions** -* 2a. The list is empty. +- 2a. The student Identifier does not exist. + - 2a1. Tutorly displays an error message indicating the student does not exist. + - Use case ends. - Use case ends. +--- -* 3a. The given index is invalid. +**Use case: Add a Session** - * 3a1. AddressBook shows an error message. +**MSS** - Use case resumes at step 2. +1. Tutor requests to add a new session with the required details (Timeslot, Subject). +2. Tutorly validates the input. +3. Tutorly adds the session to the database and confirms success. -*{More to be added}* + Use case ends. -### Non-Functional Requirements - -1. Should work on any _mainstream OS_ as long as it has Java `17` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +**Extensions** -*{More to be added}* +- 1a. Tutor does not provide all required fields. + - 1a1. Tutorly prompts for the missing information. + - Use case resumes at step 2. -### Glossary +- 2a. Tutor provides invalid input for any field. + - 2a1. Tutorly displays an appropriate error message. + - 2a2. Tutor corrects the input. + - Use case resumes at step 2. -* **Mainstream OS**: Windows, Linux, Unix, MacOS -* **Private contact detail**: A contact detail that is not meant to be shared with others +- 2b. The timeslot overlaps with another existing session. + - 2b1. Tutorly displays an error message indicating the timeslot overlaps with an existing session. + - Use case ends. --------------------------------------------------------------------------------------------------------------------- +--- -## **Appendix: Instructions for manual testing** +**Use case: Enrol a student to a session** -Given below are instructions to test the app manually. +**MSS** -
:information_source: **Note:** These instructions only provide a starting point for testers to work on; -testers are expected to do more *exploratory* testing. +1. Tutor requests to enrol a new student to an existing session by providing the student’s identifier and Session ID. +2. Tutorly validates the input. +3. Tutorly adds the student profile to the session and confirms success. -
+ Use case ends. -### Launch and shutdown +**Extensions** -1. Initial launch +- 1a. Tutor does not provide all required fields. + - 1a1. Tutorly prompts for the missing information. + - 1a2. Tutor corrects the input. + - Use case resumes at step 2. - 1. Download the jar file and copy into an empty folder +- 2a. Tutor provides invalid input for any field. + - 2a1. Tutorly displays an appropriate error message. + - 2a2. Tutor corrects the input. + - Use case resumes at step 2. - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. +-- -1. Saving window preferences +**Use case: Mark attendance for a tutoring session** - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. +**MSS** - 1. Re-launch the app by double-clicking the jar file.
- Expected: The most recent window size and location is retained. +1. Tutor requests to mark a session as attended by providing the student’s identifier, Session ID, and attendance status. +2. Tutorly validates the input. +3. Tutorly logs the attendance. +4. Tutorly confirms success. -1. _{ more test cases …​ }_ + Use case ends. -### Deleting a person +**Extensions** +- 2a. The student identifier does not exist. + - 2a1. Tutorly displays an error message showing the student identifier does not exist. + - Use case ends. -1. Deleting a person while all persons are being shown +- 2b. The Session ID does not exist. + - 2b1. Tutorly displays an error message showing the session ID does not exist. + - Use case ends. - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +- 2c. The student is not enrolled in the session. + - 2c1. Tutorly displays an error message indicating the student is not enrolled in the session. + - Use case ends. - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. +### Non-Functional Requirements - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. +1. Should work on any _mainstream OS_ as long as it has Java `17` or above installed. +2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage, ensuring that typical operations (such as loading, searching, and editing records) complete within 3 seconds. +3. The graphical user interface shall be easy to use such that a new user can complete primary workflows (e.g., adding a record or searching for a student) within 5 minutes of first use. +4. 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. +5. The codebase should be modular and well-documented, allowing for easier updates or the integration of future features. At least 90% of the codebase shall have inline or external documentation, and modules must have well-defined interfaces. +6. The source code should be open source and shall be released under an approved open source license (e.g., MIT, Apache 2.0) and published in a publicly accessible repository with minimal entry barriers. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. +### Glossary -1. _{ more test cases …​ }_ +* **Tutor**: An educator who uses Tutorly to manage student details, schedule sessions, log lesson notes, and track attendance. +* **Student Record / Student Profile**: The digital record for each student stored in Tutorly. +* **Session**: A scheduled tutoring meeting or lesson. +* **Lesson**: The content delivered during a session. -### Saving data +-------------------------------------------------------------------------------------------------------------------- -1. Dealing with missing/corrupted data files +## **Appendix: Instructions for manual testing** - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ +### Launch and Shutdown + +1. **Initial Launch** + 1. Download the jar file and copy it into an empty folder. + 2. Double-click the jar file. + - Expected: Shows the GUI with a set of sample students. The window size may not be optimal. + +2. **Saving Window Preferences** + 1. Resize the window to an optimal size. Move the window to a different location. Close the window. + 2. Re-launch the app by double-clicking the jar file. + - Expected: The most recent window size and location are retained. + +### Saving Data + +1. **Dealing with Missing Data Files** + 1. Simulate a missing file by renaming or deleting the data file. + 2. Launch the application. + - Expected: Application should handle the error gracefully, by creating a new data file containing the sample data once any command is successfully executed. (For student/session add commands, it will also include the added student/session.) + +2. **Dealing with Corrupted Data Files** + 1. Simulate a corrupted file by modifying the data file to be unreadable. + 2. Launch the application. + - Expected: Application should clear the corrupted data file and create a empty data file once any command is successfully executed. + +### Adding a Student + +1. **Adding a New Student** + 1. Test case: Add a new student with valid details. `student add n/John Doe` + - Expected: Student is added successfully, and a confirmation message is shown. + 2. Test case: Add a new student with invalid details (e.g., missing required fields `student add p/12345678`). + - Expected: Error message is shown, prompting for correct input. + +### Deleting a Student + +1. **Deleting a Student** + Prerequisite: The student must exist in the list. + 1. Test case: `student delete 1` + - Expected: Student with id 1 is deleted from the list. Details of the deleted students are shown. + 2. Test case: `student delete 0` + - Expected: No student is deleted. Error details are shown in the status message. + 3. Other incorrect delete commands to try: `student delete`, `student delete x`(where x is a number that is larger than the list size) + - Expected: Similar to previous. + +### Searching for a Student + +1. **Search for a Student** + 1. Test case: Search for a student by name. `student search n/John` + - Expected: A message "x students listed!" is shown. Matching student profiles are displayed. (x is the number of students matching the search query. If no matches are found, x = 0. If multiple matches are found, x > 1. If only one match is found, x = 1. The list of students is filtered to show only those matching the search query.) + 2. Test case: Search with a query that has no matches. `student search n/NonExistent` + - Expected: "0 students listed!" message is shown. + +### Editing a Student + +1. **Editing a Student** + 1. Test case: Edit a student’s details with valid input. `student edit 1 n/John Smith` + - Expected: Student details are updated successfully, and a confirmation message is shown. + 2. Test case: Edit a student’s details with invalid input. `student edit x n/John Smith`(x is a number larger than the list size) + - Expected: Error message "Student not found!" is shown. + 3. Test case: Edit a student’s details with missing required fields. `student edit 1 n/` + - Expected: Error message is shown, prompting for correct input. + +### Adding a Session + +1. **Adding a Session** + 1. Test case: Add a new session with valid details. `session add t/30 Mar 2025 11:30-13:30 sub/Math` + - Expected: Session is added successfully, and a confirmation message is shown. + 2. Test case: Add a new session with invalid details (e.g., missing required fields). `session add` + - Expected: Error message is shown, prompting for correct input. + 3. Test case: Add a new session with overlapping timeslots. `session add t/30 Mar 2025 11:30-13:30 sub/Science` + (Suppose there is a session whose time slot has overlapped with the new session's time slot.) + - Expected: Error message is shown, indicating the timeslot overlaps with an existing session. + +### Marking Attendance for a Session + +1. **Marking attendance for a session** + Prerequisite: The session and student must exist and the student must be enrolled in the session. + 1. Test case: Mark a session with valid input. `session mark 1 ses/1` + - Expected: Attendance is marked successfully, and a confirmation message is shown. + 2. Test case: Mark a session as completed with invalid input (e.g., invalid session id). `session mark 1 ses/x`(x is a number larger than the list size) + - Expected: Error message is shown, prompting for correct input. + 3. Test case: Mark a session which the student is not enrolled in. `session mark 1 ses/2` + - Expected: Error message is shown, indicating the student is not enrolled in the session. + +### Adding Feedback + +1. **Adding feedback for a session** + Prerequisite: The session and student must exist and the student must be enrolled in the session. + 1. Test case: Add feedback with valid input. `session feedback 1 ses/1 f/Great session!` + - Expected: Feedback is added successfully, and a confirmation message is shown. + 2. Test case: Add feedback with invalid input (e.g., missing required fields). `session feedback` + - Expected: Error message is shown, prompting for correct input. + 3. Test case: Add feedback for a session which the student is not enrolled in. `session feedback 1 ses/2 f/Great session!` + (Suppose student 1 is not enrolled in session 2) + - Expected: Error message is shown, indicating the student is not enrolled in the session. + +### Undo Feature + +1. **Undo Operations** + 1. Test case: Perform an action (e.g., add a student), then undo the action. + - Expected: The action is undone, and the previous state is restored. There is also a confirmation message shown. + +### Error Handling + +1. **Invalid Commands** + 1. Test case: Enter an invalid command. + - Expected: Error message is shown, indicating the command is not recognized/unknown. + +2. **System Errors** + 1. Test case: Simulate a system error (e.g., by corrupting a data file). + - Expected: Application handles the error gracefully, removing or replacing the corrupted data file + +These instructions provide a starting point for testers to work on; testers are expected to do more exploratory testing. + +## **Appendix: Planned Enhancements** + +1. Enhance undo functionality for the add command to allow for rollback of assigned IDs. +2. Improve the output display to seamlessly present long messages without requiring scrolling. +3. Refine cell selection behavior in the UI to reduce flickering and improve responsiveness. +4. Optimize the list view so it dynamically adjusts its height in response to changes in the filtered item count. +5. Enhance the UI focus management to ensure that the correct item is consistently highlighted. +6. Add support for multiple students of the same name. +7. A `redo` command to undo an `undo` command. +8. Improved `search` command for `student` and `session` with other fields including **tags** and **date/time range** with control over matching **any** or **all** fields. +9. `class` management commands that handle adding of **multiple** sessions and **mass** enrolling/marking of attendance for students. +10. Viewing sessions each student is enrolled in via the `students` tab. -1. _{ more test cases …​ }_ diff --git a/docs/SettingUp.md b/docs/SettingUp.md index aef33ec72fd..9c0b1d912a2 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -23,7 +23,7 @@ If you plan to use Intellij IDEA (highly recommended): 1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
:exclamation: Note: Importing a Gradle project is slightly different from importing a normal Java project. 1. **Verify the setup**: - 1. Run the `seedu.address.Main` and try a few commands. + 1. Run the `tutorly.Main` and try a few commands. 1. [Run the tests](Testing.md) to ensure they all pass. -------------------------------------------------------------------------------------------------------------------- diff --git a/docs/Testing.md b/docs/Testing.md index 8a99e82438a..d99c8e9a8ad 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -29,8 +29,8 @@ There are two ways to run tests. This project has three types of tests: 1. *Unit tests* targeting the lowest level methods/classes.
- e.g. `seedu.address.commons.StringUtilTest` + e.g. `tutorly.commons.StringUtilTest` 1. *Integration tests* that are checking the integration of multiple code units (those code units are assumed to be working).
- e.g. `seedu.address.storage.StorageManagerTest` -1. Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together.
- e.g. `seedu.address.logic.LogicManagerTest` + e.g. `tutorly.storage.StorageManagerTest` +1. Hybrids of unit and integration tests. These test are checking multiple code units as well as how they are connected together.
+ e.g. `tutorly.logic.LogicManagerTest` diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 27c2d1cf16c..733a0238fdf 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,8 +3,11 @@ 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. +Tutorly is a desktop app designed for private tutors to **manage their student and lesson records efficiently**. +It combines the speed and precision of typing commands with the convenience of a visual interface. +If you prefer using your keyboard over clicking through menus, Tutorly allows you to complete student management tasks more quickly than traditional apps. +## Table of Contents * Table of Contents {:toc} @@ -12,31 +15,56 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo ## Quick start -1. Ensure you have Java `17` or above installed in your Computer.
- **Mac users:** Ensure you have the precise JDK version prescribed [here](https://se-education.org/guides/tutorials/javaInstallationMac.html). +1. **Install Java**: Ensure you have Java `17` or above installed in your Computer. You can download the correct [JDK](#glossary) for your [operating system](#glossary) using this [link](https://www.oracle.com/sg/java/technologies/downloads/).
+ **Mac users**: Ensure you have the precise JDK version prescribed [here](https://se-education.org/guides/tutorials/javaInstallationMac.html). -1. Download the latest `.jar` file from [here](https://github.com/se-edu/addressbook-level3/releases). +2. **Download Tutorly**: Get the latest [JAR](#glossary) file from [here](https://github.com/AY2425S2-CS2103T-T17-3/tp/releases/latest/download/tutorly.jar). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +3. **Move the file**: Locate the downloaded `tutorly.jar` file and place it in a folder where you want to keep the app's data. This will be the app's _home folder_. -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
+4. **Open a [command terminal](#glossary)** - This is a tool that lets you run commands on your computer: + - On **Windows**: Press `Win + R`, type `cmd`, and press **Enter**. + - On **Mac**: Open **Terminal** from the Applications > Utilities folder. + - On **Linux**: Open **Terminal** from your applications menu. + +5. **Navigate to the folder**: In the terminal, type `cd ` (with a space after). Then, **click and hold** the _home folder_, **drag** it into the terminal window and **release** it. Doing this will automatically insert the full path of the folder. Press **Enter**. + +6. **Run Tutorly**: Type `java -jar tutorly.jar` and press **Enter**. This will start the application. A GUI similar to the below should appear in a few seconds, containing some sample data.
![Ui](images/Ui.png) +7. **UI Layout**: Feel free to take a look at the following images for a better understanding of the layout! + +### Students Tab + +![students tab](images/StudentsTab.png) + +### Student Card + +![student card](images/StudentCard.png) + +### Sessions Tab -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try: +![sessions tab](images/SessionsTab.png) - * `list` : Lists all contacts. +### Execute a command - * `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. +Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
+Some example commands you can try: - * `delete 3` : Deletes the 3rd contact shown in the current list. +* `student list` : Lists all students. - * `clear` : Deletes all contacts. +* `student add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a student named `John Doe` to the app. - * `exit` : Exits the app. +* `student delete 3` : Deletes the student with the ID 3. -1. Refer to the [Features](#features) below for details of each command. +* `session list` : List all sessions. + +* `clear` : Deletes all students and sessions. + +* `exit` : Exits the app. + +Refer to the [Features](#features) below for details of each command. + +[Back to top :arrow_up:](#table-of-contents) -------------------------------------------------------------------------------------------------------------------- @@ -46,137 +74,489 @@ 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`. +* Words in `UPPER_CASE` are the [parameters](#parameter-summary) to be supplied by the user.
+ e.g. in `student add n/NAME`, `NAME` is a parameter which can be used as `student add n/John Doe`. -* Items in square brackets are optional.
+* [STUDENT_IDENTIFIER](#glossary) can either be the target student's ID, or their full name. Examples: `John Doe` or `2`. + +* Parameters 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`. * 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. * Parameters can be in any order.
- e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. + e.g. if the command specifies `n/NAME p/PHONE`, `p/PHONE n/NAME` is also acceptable. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
- e.g. if the command specifies `help 123`, it will be interpreted as `help`. +* Extra parameters for commands that do not take in parameters ([general](#general-commands) commands, `student list` and `session list`) will be ignored.
+ e.g. if the command specifies `help 123` or `session list blah`, it will be interpreted as `help` and `session list`. * If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application.
-### Viewing help : `help` +### Parameter Summary -Shows a message explaning how to access the help page. +| Parameter | Format | Constraint | Length | Example | +|-----------|------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|--------------------|----------------------------------| +| ADDRESS | - | - | Max: 255 | Blk 30 Geylang Street 29, #06-40 | +| DATE | `dd MMM yyyy` | - | - | 11 Apr 2025 | +| EMAIL | `local-part@domain` | - | Max: 254 | hello@tutorly.com | +| FEEDBACK | - | - | Max: 200 | Did not complete homework | +| MEMO | - | - | Max: 255 | Adept at calculus | +| NAME | - | Start with a letter, and only contain letters, numbers, spaces, and these special characters: `()@*-+=:;'<>,?/.` | Max: 255 | John Doe | +| PHONE | - | Only contain numbers, spaces, hyphens, and an optional country code prefix | Min: 3
Max: 20 | +65 98765432 | +| SUBJECT | - | - | Max: 20 | Mathematics | +| TAG | - | - | Max: 20 | A-Levels | +| TIMESLOT | `dd MMM yyyy HH:mm-HH:mm` for sessions within a day,
`dd MMM yyyy HH:mm-dd MMM yyyy HH:mm` for sessions spanning multiple days | - | - | 11 Apr 2025 16:00-18:00 | -![help message](images/helpMessage.png) +[Back to top :arrow_up:](#table-of-contents) + +-------------------------------------------------------------------------------------------------------------------- + +### General Commands + +#### Viewing help: `help` + +Shows a window containing the command summary with a link to this user guide. Format: `help` +Help window: -### Adding a person: `add` +![help message](images/helpMessage.png) + +#### Clearing all data: `clear` -Adds a person to the address book. +Clears all students and sessions from the app. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +Format: `clear` + +Example output: + +![clear result](images/ClearResult.png) + +Running the [undo](#undoing-a-command-undo) command after `clear` restores all students and sessions to before the command was run. + +#### Exiting the program: `exit` + +Closes the Tutorly app. + +Format: `exit` + +#### Undoing a command: `undo` + +Undoes the last successfully executed command that has updated the data. + +Commands that update data and are thus undoable: +* The `clear` command. +* The `add`, `delete` and `edit` commands for [student](#student-management-student-action) and [session](#session-management-session-action). +* The `enrol`, `unenrol`, `mark`, `unmark` and `feedback` commands for [session](#session-management-session-action). + +Format: `undo` + +Examples: +* `session delete` followed by `undo` will undo the delete command by adding the session back. +* If the following commands were ran in order: `student edit`, `student add`, `student search`, `help`, running `undo` will undo the `student add` command. + +
:exclamation: **Caution:** +Commands that are not successfully executed due to errors will not be undone. + +After closing and re-opening the app, all previous commands will be forgotten and running `undo` will not undo any previous commands before the app was closed. +
+ +[Back to top :arrow_up:](#table-of-contents) + +-------------------------------------------------------------------------------------------------------------------- + +### Viewing tabs
:bulb: **Tip:** -A person can have any number of tags (including 0) +The main window will automatically switch to the tab which shows the results of the command that has been executed. + +Commands other than `search` will also clear any filters from previous `search` commands.
+#### Viewing students tab: `student` + +Shows the student tab in the main window. + +Format: `student` + +[Back to top :arrow_up:](#table-of-contents) + +#### Viewing sessions tab: `session` + +Shows the session tab in the main window. + +Format: `session` + +[Back to top :arrow_up:](#table-of-contents) + +-------------------------------------------------------------------------------------------------------------------- + +### Student Management: `student ACTION` + +The following commands all begin with `student` followed by an action word. + +#### Adding a student: `add` + +Adds a student to the app. + +Format: `student add n/NAME [p/PHONE] [e/EMAIL] [a/ADDRESS] [m/MEMO] [t/TAG]…​` + +* Refer to the [parameter summary](#parameter-summary) for the expected formats of the parameters. + +
:bulb: **Tip:** +A student can have any number of tags (including 0). +
+ +Examples: +* `student add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 m/loves Math` +* `student add n/Mary Jane t/olevels e/maryjane@example.com p/81234567 t/priority` + +
:exclamation: **Caution:** +Besides IDs, names are also used to uniquely identify students. Thus, duplicate students with the same name are not allowed. + +Running `student add n/Mary Jane` when a student with the name `Mary Jane` already exists will show an error as it is considered a duplicate student. +
+ +Example output: + +![student add after](images/StudentAddAfter.png) + +Running the [undo](#undoing-a-command-undo) command after `student add` removes the newly added student. + +[Back to top :arrow_up:](#table-of-contents) + +#### Listing all students: `list` + +Shows a list of all students. + +Format: `student list` + +[Back to top :arrow_up:](#table-of-contents) + +#### Viewing student card: `view` + +Scrolls to the details of the student with the specified [STUDENT_IDENTIFIER](#glossary) in the window of the student tab. + +Format: `student view STUDENT_IDENTIFIER` + 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` +* `student view 1` +* `student view John Doe` -### Listing all persons : `list` +Example output: -Shows a list of all persons in the address book. +![student id](images/StudentViewAfter.png) -Format: `list` +[Back to top :arrow_up:](#table-of-contents) -### Editing a person : `edit` +#### Editing a student: `edit` -Edits an existing person in the address book. +Edits an existing student with the specified [STUDENT_IDENTIFIER](#glossary). -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Format: `student edit STUDENT_IDENTIFIER [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [m/MEMO] [t/TAG]…​` -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ -* At least one of the optional fields must be provided. +* At least one of the optional [parameters](#parameter-summary) 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. +* When editing tags, the existing tags of the student will be removed i.e adding of tags is not cumulative. +* You can remove all the student’s tags by typing `t/` without specifying any tags after it. Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +* `student edit John Doe p/91234567 e/johndoe@example.com` Edits the phone number and email address of John Doe to be `91234567` and `johndoe@example.com` respectively. +* `student edit 2 n/Betsy Crower t/` Edits the name of the student with an ID of 2 to be `Betsy Crower` and clears all existing tags. -### Locating persons by name: `find` +
:exclamation: **Caution:** +Besides IDs, names are also used to uniquely identify students. Thus, duplicate students with the same name are not allowed. + +Running `student edit 3 n/Betsy Crower` when another student named `Betsy Crower` already exists will show an error as it is considered a duplicate student. +
+ +Example output: + +![student edit after](images/StudentEditAfter.png) + +Running the [undo](#undoing-a-command-undo) command after `student edit` reverts the student's details back to before the edit was made. + +[Back to top :arrow_up:](#table-of-contents) -Finds persons whose names contain any of the given keywords. +#### Searching for students: `search` -Format: `find KEYWORD [MORE_KEYWORDS]` +Finds students whose names or phone numbers contain any of the given keywords, or is enrolled to a specific session. -* 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` -* Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +Format: `student search [ses/SESSION_ID] [n/NAME_KEYWORDS] [p/PHONE_KEYWORDS]` + +* The keywords are case-insensitive and order does not matter. e.g. `hans bo` will match `Bo Hans` +* Incomplete words will still be matched e.g. `Han` will match `Hans` or `8765` will match `91238765` +* Students matching at least one keyword **or** are enrolled to the session will be returned. + +
:exclamation: **Caution:** +`SESSION_ID` only accepts one valid positive number that corresponds with an existing session's ID. + +This will be updated in future versions to allow the searching of multiple IDs. +
Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +* `student search n/John p/9123 8765` returns `johnathan`, `John Doe` and other students with a phone number that contains `9123` or `8765`. +* `student search ses/3 n/alex david` returns `Alex Yeoh`, `David Li` and other students who attended session with the id 3. +* `student search` will simply return all students. + +Example output (with matching keywords highlighted): -### Deleting a person : `delete` +![student search after](images/StudentSearchAfter.png) -Deletes the specified person from the address book. +[Back to top :arrow_up:](#table-of-contents) -Format: `delete INDEX` +#### Deleting a student: `delete` -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index **must be a positive integer** 1, 2, 3, …​ +Deletes the student with the specified [STUDENT_IDENTIFIER](#glossary). + +Format: `student delete STUDENT_IDENTIFIER` 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. +* `student delete 2` deletes the student with the ID of 2. +* `student delete John Doe` deletes the student with the name `John Doe`. -### Clearing all entries : `clear` +Running the [undo](#undoing-a-command-undo) command after `student delete` adds the deleted student back. -Clears all entries from the address book. +[Back to top :arrow_up:](#table-of-contents) -Format: `clear` +-------------------------------------------------------------------------------------------------------------------- -### Exiting the program : `exit` +### Session Management: `session ACTION` -Exits the program. +The following commands all begin with `session` followed by an action word. -Format: `exit` +#### Adding a session: `add` + +Adds a session to the app. + +Format: `session add t/TIMESLOT sub/SUBJECT` + +* Refer to the [parameter summary](#parameter-summary) for the expected format of `TIMESLOT` and `SUBJECT`. + +Examples: +* `session add t/30 Mar 2025 11:30-13:30 sub/Math` adds a session with the subject `Math` on 30 March 2025 from 11.30am to 13.30pm. +* `session add t/30 Mar 2025 23:00-31 Mar 2025 01:00 sub/Eng` adds a session with the subject `Eng` which lasts 2 hours from 30 March 2025 11pm to 31 March 2025 1am. + +Running the [undo](#undoing-a-command-undo) command after `session add` removes the newly added session. + +Example output: + +![session add after](images/SessionAddAfter.png) + +[Back to top :arrow_up:](#table-of-contents) + +#### Listing all sessions: `list` + +Shows a list of all sessions. + +Format: `session list` + +[Back to top :arrow_up:](#table-of-contents) + +#### Viewing attendance for a session: `view` + +Shows the attendance of students for a given session. + +Format: `session view SESSION_ID` + +Examples: +* `session view 1` +* `session view 5` + +Example Output: + +![session view after](images/SessionViewAfter.png) + +[Back to top :arrow_up:](#table-of-contents) + +#### Editing a session: `edit` + +Edits an existing session with the specified `SESSION_ID`. + +Format: `session edit SESSION_ID [t/TIMESLOT] [sub/SUBJECT]` + +* At least one of the optional [parameters](#parameter-summary) must be provided. +* Existing values will be updated to the input values. + +Examples: +* `session edit 3 t/11 Apr 2025 11:30-13:30` Edits the date of the session with the ID 3 to be on 11 April 2025 from 11.30am to 1.30pm. +* `session edit 2 t/20 June 2025 23:00-21 June 2025 01:00 sub/Math` Edits the date and subject of the session with the ID 2 to last from 20 June 2025 11pm to 21 June 2025 1am with the subject `Math`. + +Running the [undo](#undoing-a-command-undo) command after `session edit` reverts the session's details back to before the edit was made. + +[Back to top :arrow_up:](#table-of-contents) + +#### Searching for sessions: `search` + +Finds sessions on a particular date or on a subject which matches any of the given keywords. + +Format: `session search [d/DATE] [sub/SUBJECT_KEYWORDS]` + +* Refer to the [parameter summary](#parameter-summary) for the expected format of `DATE`. +* The keywords are case-insensitive and order does not matter. e.g. `math eng` will match `Eng Math` +* Incomplete words will still be matched e.g. `Mat` will match `Math` +* Sessions whose timeslots contain the given date or have a subject that match at least one keyword will be returned. + +Examples: +* `session search d/22 May 2025` returns sessions with timeslots that include 22 May 2025. +* `session search sub/Math d/11 Jun 2025` returns sessions with subjects `Math`, `Mathematics` and sessions with timeslots that include 11 June 2025. +* `session search` will simply return all sessions. + +Example output (with matching keywords and date highlighted): + +![session search after](images/SessionSearchAfter.png) + +[Back to top :arrow_up:](#table-of-contents) + +#### Deleting a session: `delete` + +Deletes the session with the specified `SESSION_ID`. + +Format: `session delete SESSION_ID` + +Examples: +* `session delete 2` deletes the session with the ID of 2. + +Running the [undo](#undoing-a-command-undo) command after `session delete` adds the deleted session back. + +[Back to top :arrow_up:](#table-of-contents) + +#### Enrolling a student to a session: `enrol` + +Enrols a student with the specified [STUDENT_IDENTIFIER](#glossary) to a specific session. + +Format: `session enrol STUDENT_IDENTIFIER ses/SESSION_ID` + +* The attendance for the student to the session upon enrolment is marked as absent by default. + +Examples: +* `session enrol 2 ses/3` enrols a student with an ID of 2 to attend a session with an ID of 3. +* `session enrol John Doe ses/4` enrols a student with the name `John Doe` to attend a session with an ID of 4. + +Example output: + +![session enrol after](images/SessionEnrolAfter.png) + +Running the [undo](#undoing-a-command-undo) command after `session enrol` will unenrol the student from the session. + +[Back to top :arrow_up:](#table-of-contents) + +#### Unenrolling a student from a session: `unenrol` + +Unenrols a student with the specified [STUDENT_IDENTIFIER](#glossary) from a session. + +Format: `session unenrol STUDENT_IDENTIFIER ses/SESSION_ID` + +Examples: +* `session unenrol 2 ses/3` unenrols a student with an ID of 2 from a session with an ID of 3. +* `session unenrol John Doe ses/4` unenrols a student with the name `John Doe` from a session with an ID of 4. + +Running the [undo](#undoing-a-command-undo) command after `session unenrol` will enrol the student back into the session. + +[Back to top :arrow_up:](#table-of-contents) + +#### Marking attendance: `mark` + +Marks the attendance of a student with the specified [STUDENT_IDENTIFIER](#glossary) for a session. + +Format: `session mark STUDENT_IDENTIFIER ses/SESSION_ID` + +* Note that only students who are enrolled in the session can be marked as present. + +Examples: +* `session mark 2 ses/3` marks the attendance for the student with an ID of 2 for a session with an ID of 3 as present. +* `session mark John Doe ses/4` marks the attendance for a student with the name `John Doe` for a session with an ID of 4 as present. + +Running the [undo](#undoing-a-command-undo) command after `session mark` will unmark the student's attendance in the session. + +
:bulb: **Tip:** +You can also click on the checkbox next to a student's name in a session's attendance list to toggle the marking of attendance. +
+ +[Back to top :arrow_up:](#table-of-contents) + +#### Unmarking attendance: `unmark` + +Unmarks the attendance of a student with the specified [STUDENT_IDENTIFIER](#glossary) for a session. + +Format: `session unmark STUDENT_IDENTIFIER ses/SESSION_ID` + +* Note that only students who are enrolled in the session can be unmarked. + +Examples: +* `session unmark 2 ses/3` unmarks the attendance for the student with an ID of 2 for a session with an ID of 3. +* `session unmark John Doe ses/4` unmarks the attendance for a student with the name `John Doe` for a session with an ID of 4. + +Running the [undo](#undoing-a-command-undo) command after `session unmark` will mark the student's attendance in the session. + +
:bulb: **Tip:** +You can also click on the checkbox next to a student's name in a session's attendance list to toggle the marking of attendance. +
+ +[Back to top :arrow_up:](#table-of-contents) + +#### Adding or updating feedback: `feedback` + +Adds or updates the feedback for a student with the specified [STUDENT_IDENTIFIER](#glossary) in a session. + +Format: `session feedback STUDENT_IDENTIFIER ses/SESSION_ID f/FEEDBACK` + +* Refer to the [parameter summary](#parameter-summary) for the expected format of `FEEDBACK`. +* If no feedback exists, a new feedback will be added for the student in the session. +* If a feedback already exists, the old feedback will be overwritten, i.e. Only one feedback is allowed per student per session. +* You can remove a previously set feedback by typing f/ without specifying any feedback after it. +* Feedback for a student is viewable in the attendance list of a session. + +Examples: +* `session feedback 2 ses/3 f/Good Job!` updates the feedback `Good Job!` for the student with an ID of 2 for a session with an ID of 3. +* `session feedback John Doe ses/4 f/Sick leave` updates the feedback `Sick leave` for a student with the name `John Doe` for a session with an ID of 4. +* `session feedback Betsy Crower ses/4 f/` clears the feedback for a student with the name `Betsy Crower` for a session with an ID of 4. + +Example output: + +![session feedback after](images/SessionFeedbackAfter.png) + +Running the [undo](#undoing-a-command-undo) command after `session feedback` reverts the feedback back to before the command was run. + +[Back to top :arrow_up:](#table-of-contents) + +-------------------------------------------------------------------------------------------------------------------- ### 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. +Tutorly data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. ### Editing the data file -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +Tutorly data are saved automatically as a JSON file `[home_folder]/data/tutorly.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, Tutorly 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 Tutorly 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]` - -_Details coming soon ..._ +[Back to top :arrow_up:](#table-of-contents) -------------------------------------------------------------------------------------------------------------------- ## FAQ **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous Tutorly _home folder_. + +**Q**: What is the difference between the `student list` and `student` commands?
+**A**: The `student list` command will list all students in Tutorly. The `student` command simply switches the active tab to students and preserves results from any previous `student search` commands. The same applies to `session` and `session list`. + +**Q**: What is the difference between the `memo` and `feedback`?
+**A**: `Memo` is a short note you can add on to a student's details, while `feedback` is specifically for a student's performance in a particular session. + +**Q**: How should I fill in the `STUDENT_IDENTIFIER` parameter?
+**A**: The [`STUDENT_IDENTIFIER`](#glossary) can either be the student's ID or their full name, both of which are viewable from the student tab in the app. + +[Back to top :arrow_up:](#table-of-contents) -------------------------------------------------------------------------------------------------------------------- @@ -184,17 +564,70 @@ _Details coming soon ..._ 1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. 2. **If you minimize the Help Window** and then run the `help` command (or use the `Help` menu, or the keyboard shortcut `F1`) again, the original Help Window will remain minimized, and no new Help Window will appear. The remedy is to manually restore the minimized Help Window. +3. **For commands that should focus on a specific item in the window**, like after a `student add` or `session enrol`, the window may sometimes not scroll to the target item due to UI loading behaviour. The remedy is to run the `view` command again for the window to scroll to the target item, or use the scrollbar in the tab. + +[Back to top :arrow_up:](#table-of-contents) -------------------------------------------------------------------------------------------------------------------- ## Command summary -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +| Context | Action | Format | Examples | +|---------|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|--------------------------------------------------------| +| General | [Help](#viewing-help-help) | `help` | - | +| | [Clear data](#clearing-all-data-clear) | `clear` | - | +| | [Exit](#exiting-the-program-exit) | `exit` | - | +| | [Undo command](#undoing-a-command-undo) | `undo` | - | +| Tab | [Show students tab](#viewing-students-tab-student) | `student` | - | +| | [Show session tab](#viewing-sessions-tab-session) | `session` | - | +| Student | [Add](#adding-a-student-add) | `student add n/NAME [p/PHONE] [e/EMAIL] [a/ADDRESS] [m/MEMO] [t/TAG]…​` | `student add n/John Doe p/98765432` | +| | [List](#listing-all-students-list) | `student list` | - | +| | [View card](#viewing-student-card-view) | `student view STUDENT_IDENTIFIER` | `student view 1` or `student view John Doe` | +| | [Edit](#editing-a-student-edit) | `student edit STUDENT_IDENTIFIER [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [m/MEMO] [t/TAG]…​` | `student edit 2 n/James Lee p/91234567` | +| | [Search](#searching-for-students-search) | `student search [ses/SESSION_ID] [n/NAME_KEYWORDS] [p/PHONE_KEYWORDS]` | `student search n/alex dav p/9123 8765` | +| | [Delete](#deleting-a-student-delete) | `student delete STUDENT_IDENTIFIER` | `student delete 3` | +| Session | [Add](#adding-a-session-add) | `session add t/TIMESLOT sub/SUBJECT` | `session add t/30 Mar 2025 11:30-13:30 sub/Math` | +| | [List](#listing-all-sessions-list) | `session list` | - | +| | [View attendance](#viewing-attendance-for-a-session-view) | `session view SESSION_ID` | `session view 4` | +| | [Edit](#editing-a-session-edit) | `session edit SESSION_ID [t/TIMESLOT] [sub/SUBJECT]` | `session edit 2 t/11 Jun 2025 11:30-13:30 sub/English` | +| | [Search](#searching-for-sessions-search) | `session search [d/DATE] [sub/SUBJECT_KEYWORDS]` | `session search d/2025-04-15 sub/Math Eng` | +| | [Delete](#deleting-a-session-delete) | `session delete SESSION_ID` | `session delete 1` | +| | [Enrol student](#enrolling-a-student-to-a-session-enrol) | `session enrol STUDENT_IDENTIFIER ses/SESSION_ID` | `session enrol 4 ses/3` | +| | [Unenrol student](#unenrolling-a-student-from-a-session-unenrol) | `session unenrol STUDENT_IDENTIFIER ses/SESSION_ID` | `session unenrol 4 ses/3` | +| | [Mark attendance](#marking-attendance-mark) | `session mark STUDENT_IDENTIFIER ses/SESSION_ID` | `session mark John Doe ses/2` | +| | [Unmark attendance](#unmarking-attendance-unmark) | `session unmark STUDENT_IDENTIFIER ses/SESSION_ID` | `session unmark 3 ses/2` | +| | [Add or Update feedback](#adding-or-updating-feedback-feedback) | `session feedback STUDENT_IDENTIFIER ses/SESSION_ID f/FEEDBACK` | `session feedback 3 ses/2 f/Good Job!` | + +[Back to top :arrow_up:](#table-of-contents) + +-------------------------------------------------------------------------------------------------------------------- + +## Glossary + +| Term | Definition | +|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Command Terminal | A tool on a computer where you type instructions to make the computer perform tasks. | +| Home folder | The folder which contains the `tutorly.jar` file and data folder. | +| JAR | Java Archive file: A package that bundles a Java program and all its necessary parts into a single file, making it easier to share and run. | +| JDK | Java Development Kit: A software package that provides everything needed to create and run Java programs. | +| Operating System | An operating system is the main software that manages a computer’s hardware and allows you to run applications. Some examples include `Windows`, `Mac` and `Linux`. | +| Parameters | These are placeholders in a command that users replace with specific information to customize the command's action. They are usually prefixed with letters like `n/` or `p/`. | +| STUDENT_IDENTIFIER | A parameter used to identify a student. It can either be the student's ID (a positive number), or their full name. | + +[Back to top :arrow_up:](#table-of-contents) + +## Coming soon + +Planned features that will be added in the coming versions. + +1. A `redo` command in case you want to undo your `undo`. +2. Improved `search` command for `student` and `session` with other fields including **tags** and **date/time range** with control over matching **any** or **all** fields. +3. `class` management commands that handles adding of **multiple** sessions and **mass** enrolling/marking of attendance for students. +4. Viewing sessions each student is enrolled in via the `students` tab. +5. Add support for multiple students of the same name. +6. Better auto scroll and focus on target item being updated in window. +7. Seamlessly present long messages without requiring scrolling. + +[Back to top :arrow_up:](#table-of-contents) + +-------------------------------------------------------------------------------------------------------------------- diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..3e80bc4c079 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "Tutorly" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2425S2-CS2103T-T17-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..69244d37219 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: "Tutorly"; font-size: 32px; } } diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index 48b6cc4333c..9ce63455b0e 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -8,12 +8,18 @@ Participant ":Logic" as logic LOGIC_COLOR Participant ":Model" as model MODEL_COLOR Participant ":Storage" as storage STORAGE_COLOR -user -[USER_COLOR]> ui : "delete 1" +user -[USER_COLOR]> ui : "student delete 1" activate ui UI_COLOR -ui -[UI_COLOR]> logic : execute("delete 1") +ui -[UI_COLOR]> logic : execute("student delete 1") activate logic LOGIC_COLOR +logic -[LOGIC_COLOR]> model : getPersonByIdentity(...) +activate model MODEL_COLOR + +model -[MODEL_COLOR]-> logic : p +deactivate model + logic -[LOGIC_COLOR]> model : deletePerson(p) activate model MODEL_COLOR diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..93b04fa806a 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -6,16 +6,20 @@ skinparam classBackgroundColor MODEL_COLOR AddressBook *-right-> "1" UniquePersonList AddressBook *-right-> "1" UniqueTagList +AddressBook *-left-> "1" UniqueSessionList +AddressBook *-down-> "1" UniqueAttendanceRecordList + UniqueTagList -[hidden]down- UniquePersonList UniqueTagList -[hidden]down- UniquePersonList -UniqueTagList -right-> "*" Tag -UniquePersonList -right-> Person +UniqueTagList o-right-> "*" Tag +UniquePersonList o-right-> "*" Person +UniqueSessionList o-left-> "*" Session +UniqueAttendanceRecordList o-down-> "*" AttendanceRecord Person -up-> "*" Tag -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address +Person "1" --o "*" AttendanceRecord +Session "1" --o "*" AttendanceRecord + @enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 5241e79d7da..beab4aaa57a 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -5,8 +5,10 @@ skinparam ArrowFontStyle plain box Logic LOGIC_COLOR_T1 participant ":LogicManager" as LogicManager LOGIC_COLOR participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR -participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR -participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR +participant ":StudentCommandParser" as StudentCommandParser LOGIC_COLOR +participant ":DeleteStudentCommandParser" as DeleteStudentCommandParser LOGIC_COLOR +participant "i:Identity" as Identity LOGIC_COLOR +participant "d:DeleteStudentCommand" as DeleteStudentCommand LOGIC_COLOR participant "r:CommandResult" as CommandResult LOGIC_COLOR end box @@ -14,56 +16,85 @@ box Model MODEL_COLOR_T1 participant "m:Model" as Model MODEL_COLOR end box -[-> LogicManager : execute("delete 1") +[-> LogicManager : execute("student delete 1") activate LogicManager -LogicManager -> AddressBookParser : parseCommand("delete 1") +LogicManager -> AddressBookParser : parse("student delete 1") activate AddressBookParser -create DeleteCommandParser -AddressBookParser -> DeleteCommandParser -activate DeleteCommandParser +create StudentCommandParser +AddressBookParser -> StudentCommandParser +activate StudentCommandParser -DeleteCommandParser --> AddressBookParser -deactivate DeleteCommandParser +StudentCommandParser --> AddressBookParser +deactivate StudentCommandParser -AddressBookParser -> DeleteCommandParser : parse("1") -activate DeleteCommandParser +AddressBookParser -> StudentCommandParser : parse("delete 1") +activate StudentCommandParser -create DeleteCommand -DeleteCommandParser -> DeleteCommand -activate DeleteCommand +create DeleteStudentCommandParser +StudentCommandParser -> DeleteStudentCommandParser +activate DeleteStudentCommandParser -DeleteCommand --> DeleteCommandParser : -deactivate DeleteCommand +DeleteStudentCommandParser --> StudentCommandParser +deactivate DeleteStudentCommandParser -DeleteCommandParser --> AddressBookParser : d -deactivate DeleteCommandParser +StudentCommandParser -> DeleteStudentCommandParser : parse("1") +activate DeleteStudentCommandParser + +create Identity +DeleteStudentCommandParser -> Identity +activate Identity + +Identity --> DeleteStudentCommandParser +deactivate Identity + +create DeleteStudentCommand +DeleteStudentCommandParser -> DeleteStudentCommand : DeleteStudentCommand(i) +activate DeleteStudentCommand + +DeleteStudentCommand --> DeleteStudentCommandParser : +deactivate DeleteStudentCommand + +DeleteStudentCommandParser --> StudentCommandParser : d +deactivate DeleteStudentCommandParser 'Hidden arrow to position the destroy marker below the end of the activation bar. -DeleteCommandParser -[hidden]-> AddressBookParser -destroy DeleteCommandParser +DeleteStudentCommandParser -[hidden]-> StudentCommandParser +destroy DeleteStudentCommandParser + +StudentCommandParser --> AddressBookParser : d +deactivate StudentCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +StudentCommandParser -[hidden]-> AddressBookParser +destroy StudentCommandParser AddressBookParser --> LogicManager : d deactivate AddressBookParser -LogicManager -> DeleteCommand : execute(m) -activate DeleteCommand +LogicManager -> DeleteStudentCommand : execute(m) +activate DeleteStudentCommand + +DeleteStudentCommand -> Model : getPersonByIdentity(i) +activate Model + +Model --> DeleteStudentCommand : p +deactivate Model -DeleteCommand -> Model : deletePerson(1) +DeleteStudentCommand -> Model : deletePerson(p) activate Model -Model --> DeleteCommand +Model --> DeleteStudentCommand deactivate Model create CommandResult -DeleteCommand -> CommandResult +DeleteStudentCommand -> CommandResult activate CommandResult -CommandResult --> DeleteCommand +CommandResult --> DeleteStudentCommand deactivate CommandResult -DeleteCommand --> LogicManager : r -deactivate DeleteCommand +DeleteStudentCommand --> LogicManager : r +deactivate DeleteStudentCommand [<--LogicManager deactivate LogicManager diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index 58b4f602ce6..be6cd1e9ace 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -39,7 +39,7 @@ LogicManager --> Storage Storage --[hidden] Model Command .[hidden]up.> Storage Command .right.> Model -note right of XYZCommand: XYZCommand = AddCommand, \nFindCommand, etc +note right of XYZCommand: XYZCommand = AddStudentCommand, \nSearchSessionCommand, etc Logic ..> CommandResult LogicManager .down.> CommandResult diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..5bf387096a8 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -1,54 +1,71 @@ @startuml !include style.puml + skinparam arrowThickness 1.1 skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR +skinparam Padding 4 + +Package Model as ModelPackage <> { + Class "<>\nReadOnlyAddressBook" as ReadOnlyAddressBook + Class "<>\nReadOnlyUserPrefs" as ReadOnlyUserPrefs + Class "<>\nModel" as Model + Class AddressBook + Class ModelManager + Class UserPrefs -Package Model as ModelPackage <>{ -Class "<>\nReadOnlyAddressBook" as ReadOnlyAddressBook -Class "<>\nReadOnlyUserPrefs" as ReadOnlyUserPrefs -Class "<>\nModel" as Model -Class AddressBook -Class ModelManager -Class UserPrefs - -Class UniquePersonList -Class Person -Class Address -Class Email -Class Name -Class Phone -Class Tag - -Class I #FFFFFF + Class UniquePersonList + Class UniqueList + Class UniqueSessionList + Class UniqueAttendanceRecordList + Class Person + Class Address + Class Email + Class Name + Class Phone + Class Tag + Class Memo + Class Session + Class Timeslot + Class Subject + Class AttendanceRecord + Class Feedback } Class HiddenOutside #FFFFFF HiddenOutside ..> Model AddressBook .up.|> ReadOnlyAddressBook - ModelManager .up.|> Model -Model .right.> ReadOnlyUserPrefs -Model .left.> ReadOnlyAddressBook -ModelManager -left-> "1" AddressBook -ModelManager -right-> "1" UserPrefs + +Model .left.> ReadOnlyUserPrefs +Model .right.> ReadOnlyAddressBook +ModelManager -right-> "1" AddressBook +ModelManager -left-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address +Person *--> "1" Name +Person *--> "1" Phone +Person *--> "1" Email +Person *--> "1" Address +Person *--> "1" Memo Person *--> "*" Tag -Person -[hidden]up--> I -UniquePersonList -[hidden]right-> I +UniquePersonList -up--|> UniqueList +UniqueSessionList -up--|> UniqueList +UniqueAttendanceRecordList -up--|> UniqueList +AddressBook *--> "1" UniquePersonList +AddressBook *--> "1" UniqueSessionList +AddressBook *--> "1" UniqueAttendanceRecordList + +UniqueSessionList --> "~* all" Session +Session *--> "1" Timeslot +Session *--> "1" Subject -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +UniqueAttendanceRecordList --> "~* all" AttendanceRecord +AttendanceRecord *--> "1" Feedback ModelManager --> "~* filtered" Person +ModelManager --> "~* filtered" Session @enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..76c2dc6e9b9 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -20,6 +20,8 @@ Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson Class JsonAdaptedTag +Class JsonAdaptedSession +Class JsonAdaptedAttendanceRecord } } @@ -38,6 +40,8 @@ JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson +JsonSerializableAddressBook --> "*" JsonAdaptedSession +JsonSerializableAddressBook --> "*" JsonAdaptedAttendanceRecord JsonAdaptedPerson --> "*" JsonAdaptedTag @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..c6e1beef885 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -7,12 +7,17 @@ skinparam classBackgroundColor UI_COLOR package UI <>{ Class "<>\nUi" as Ui Class "{abstract}\nUiPart" as UiPart +Class "{abstract}\nListPanel" as ListPanel Class UiManager Class MainWindow Class HelpWindow Class ResultDisplay Class PersonListPanel Class PersonCard +Class SessionListPanel +Class SessionCard +Class AttendanceRecordListPanel +Class AttendanceRecordCard Class StatusBarFooter Class CommandBox } @@ -28,33 +33,48 @@ Class HiddenLogic #FFFFFF Class HiddenOutside #FFFFFF HiddenOutside ..> Ui -UiManager .left.|> Ui +UiManager .right.|> Ui UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" SessionListPanel +MainWindow *-down-> "1" AttendanceRecordListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow PersonListPanel -down-> "*" PersonCard +SessionListPanel -down-> "*" SessionCard +AttendanceRecordListPanel -down-> "*" AttendanceRecordCard MainWindow -left-|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart -PersonListPanel --|> UiPart +PersonListPanel ---|> ListPanel PersonCard --|> UiPart +SessionListPanel ---|> ListPanel +SessionCard --|> UiPart +AttendanceRecordListPanel ---|> ListPanel +AttendanceRecordCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +ListPanel --|> UiPart PersonCard ..> Model +SessionCard ..> Model +AttendanceRecordCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic PersonListPanel -[hidden]left- HelpWindow +SessionListPanel -[hidden]left- PersonListPanel +AttendanceRecordListPanel -[hidden]left- SessionListPanel HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay ResultDisplay -[hidden]left- StatusBarFooter - +SessionCard -[hidden]left- PersonCard +AttendanceRecordCard -[hidden]left- SessionCard +UiPart -[hidden]down-> Model MainWindow -[hidden]-|> UiPart @enduml diff --git a/docs/diagrams/UndoSequenceDiagram-Logic.puml b/docs/diagrams/UndoSequenceDiagram-Logic.puml index e57368c5159..7b9ccd20601 100644 --- a/docs/diagrams/UndoSequenceDiagram-Logic.puml +++ b/docs/diagrams/UndoSequenceDiagram-Logic.puml @@ -5,16 +5,58 @@ skinparam ArrowFontStyle plain box Logic LOGIC_COLOR_T1 participant ":LogicManager" as LogicManager LOGIC_COLOR participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant "a:AddStudentCommand" as AddStudentCommand LOGIC_COLOR participant "u:UndoCommand" as UndoCommand LOGIC_COLOR +participant "d:DeleteStudentCommand" as DeleteStudentCommand LOGIC_COLOR end box box Model MODEL_COLOR_T1 participant ":Model" as Model MODEL_COLOR end box -[-> LogicManager : execute(undo) + +[-> LogicManager : execute("student add n/John Doe") activate LogicManager -LogicManager -> AddressBookParser : parseCommand(undo) +LogicManager -> AddressBookParser : parse("student add n/John Doe") +activate AddressBookParser + +create AddStudentCommand +AddressBookParser -> AddStudentCommand +activate AddStudentCommand + +AddStudentCommand --> AddressBookParser +deactivate AddStudentCommand + +AddressBookParser --> LogicManager : a +deactivate AddressBookParser + +LogicManager -> AddStudentCommand : execute() +activate AddStudentCommand + +AddStudentCommand -> Model : addPerson +activate Model + +Model --> AddStudentCommand +deactivate Model + +create DeleteStudentCommand +AddStudentCommand -> DeleteStudentCommand +activate DeleteStudentCommand +DeleteStudentCommand --> AddStudentCommand +deactivate DeleteStudentCommand + +AddStudentCommand --> LogicManager : result +deactivate AddStudentCommand +AddStudentCommand -[hidden]-> LogicManager : result +destroy AddStudentCommand + +[<--LogicManager +deactivate LogicManager + +[-> LogicManager : execute("undo") +activate LogicManager + +LogicManager -> AddressBookParser : parse("undo") activate AddressBookParser create UndoCommand @@ -30,17 +72,25 @@ deactivate AddressBookParser LogicManager -> UndoCommand : execute() activate UndoCommand -UndoCommand -> Model : undoAddressBook() -activate Model - -Model --> UndoCommand -deactivate Model - UndoCommand --> LogicManager : result deactivate UndoCommand UndoCommand -[hidden]-> LogicManager : result destroy UndoCommand +LogicManager -> DeleteStudentCommand : execute() +activate DeleteStudentCommand + +DeleteStudentCommand -> Model : deletePerson +activate Model + +Model --> DeleteStudentCommand +deactivate Model + +DeleteStudentCommand --> LogicManager : result +deactivate DeleteStudentCommand +DeleteStudentCommand -[hidden]-> LogicManager : result +destroy DeleteStudentCommand + [<--LogicManager deactivate LogicManager @enduml diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 37ad06a2803..519e1ac44de 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png deleted file mode 100644 index 02a42e35e76..00000000000 Binary files a/docs/images/BetterModelClassDiagram.png and /dev/null differ diff --git a/docs/images/ClearResult.png b/docs/images/ClearResult.png new file mode 100644 index 00000000000..e45f9f5899e Binary files /dev/null and b/docs/images/ClearResult.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index ac2ae217c51..d57759b5657 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index fe91c69efe7..8f7d2af5518 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..92866f80a0f 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/SessionAddAfter.png b/docs/images/SessionAddAfter.png new file mode 100644 index 00000000000..29fe5d6cb8c Binary files /dev/null and b/docs/images/SessionAddAfter.png differ diff --git a/docs/images/SessionEnrolAfter.png b/docs/images/SessionEnrolAfter.png new file mode 100644 index 00000000000..b5e0d83678c Binary files /dev/null and b/docs/images/SessionEnrolAfter.png differ diff --git a/docs/images/SessionFeedbackAfter.png b/docs/images/SessionFeedbackAfter.png new file mode 100644 index 00000000000..3a109b99bef Binary files /dev/null and b/docs/images/SessionFeedbackAfter.png differ diff --git a/docs/images/SessionSearchAfter.png b/docs/images/SessionSearchAfter.png new file mode 100644 index 00000000000..349a2898208 Binary files /dev/null and b/docs/images/SessionSearchAfter.png differ diff --git a/docs/images/SessionViewAfter.png b/docs/images/SessionViewAfter.png new file mode 100644 index 00000000000..912a2107036 Binary files /dev/null and b/docs/images/SessionViewAfter.png differ diff --git a/docs/images/SessionsTab.png b/docs/images/SessionsTab.png new file mode 100644 index 00000000000..e58bc224bba Binary files /dev/null and b/docs/images/SessionsTab.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..b86c6de2d57 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/StudentAddAfter.png b/docs/images/StudentAddAfter.png new file mode 100644 index 00000000000..447f87185a2 Binary files /dev/null and b/docs/images/StudentAddAfter.png differ diff --git a/docs/images/StudentCard.png b/docs/images/StudentCard.png new file mode 100644 index 00000000000..b6f61867e2f Binary files /dev/null and b/docs/images/StudentCard.png differ diff --git a/docs/images/StudentEditAfter.png b/docs/images/StudentEditAfter.png new file mode 100644 index 00000000000..d55537bfb59 Binary files /dev/null and b/docs/images/StudentEditAfter.png differ diff --git a/docs/images/StudentSearchAfter.png b/docs/images/StudentSearchAfter.png new file mode 100644 index 00000000000..46a9b349bfc Binary files /dev/null and b/docs/images/StudentSearchAfter.png differ diff --git a/docs/images/StudentViewAfter.png b/docs/images/StudentViewAfter.png new file mode 100644 index 00000000000..256f4d91fb5 Binary files /dev/null and b/docs/images/StudentViewAfter.png differ diff --git a/docs/images/StudentsTab.png b/docs/images/StudentsTab.png new file mode 100644 index 00000000000..69e39e430bb Binary files /dev/null and b/docs/images/StudentsTab.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..a9c85f71827 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..a4c750cbf36 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UiLayout.png b/docs/images/UiLayout.png new file mode 100644 index 00000000000..97fc22ac32c Binary files /dev/null and b/docs/images/UiLayout.png differ diff --git a/docs/images/UndoRedoState0.png b/docs/images/UndoRedoState0.png deleted file mode 100644 index c5f91b58533..00000000000 Binary files a/docs/images/UndoRedoState0.png and /dev/null differ diff --git a/docs/images/UndoRedoState1.png b/docs/images/UndoRedoState1.png deleted file mode 100644 index 2d3ad09c047..00000000000 Binary files a/docs/images/UndoRedoState1.png and /dev/null differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png deleted file mode 100644 index 20853694e03..00000000000 Binary files a/docs/images/UndoRedoState2.png and /dev/null differ diff --git a/docs/images/UndoRedoState3.png b/docs/images/UndoRedoState3.png deleted file mode 100644 index 1a9551b31be..00000000000 Binary files a/docs/images/UndoRedoState3.png and /dev/null differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png deleted file mode 100644 index 46dfae78c94..00000000000 Binary files a/docs/images/UndoRedoState4.png and /dev/null differ diff --git a/docs/images/UndoRedoState5.png b/docs/images/UndoRedoState5.png deleted file mode 100644 index f45889b5fdf..00000000000 Binary files a/docs/images/UndoRedoState5.png and /dev/null differ diff --git a/docs/images/UndoSequenceDiagram-Logic.png b/docs/images/UndoSequenceDiagram-Logic.png deleted file mode 100644 index 78e95214294..00000000000 Binary files a/docs/images/UndoSequenceDiagram-Logic.png and /dev/null differ diff --git a/docs/images/UndoSequenceDiagram-Model.png b/docs/images/UndoSequenceDiagram-Model.png deleted file mode 100644 index f0f3da6ae50..00000000000 Binary files a/docs/images/UndoSequenceDiagram-Model.png and /dev/null differ diff --git a/docs/images/UndoSequenceDiagram.png b/docs/images/UndoSequenceDiagram.png new file mode 100644 index 00000000000..b828aa9c432 Binary files /dev/null and b/docs/images/UndoSequenceDiagram.png differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png deleted file mode 100644 index 235da1c273e..00000000000 Binary files a/docs/images/findAlexDavidResult.png and /dev/null differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..bbb68efbd3f 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/neilchen863.png b/docs/images/neilchen863.png new file mode 100644 index 00000000000..4b0372f9276 Binary files /dev/null and b/docs/images/neilchen863.png differ diff --git a/docs/images/nickt121.png b/docs/images/nickt121.png new file mode 100644 index 00000000000..bfd39f68326 Binary files /dev/null and b/docs/images/nickt121.png differ diff --git a/docs/images/searchAlexDavidResult.png b/docs/images/searchAlexDavidResult.png new file mode 100644 index 00000000000..f904bce9dd5 Binary files /dev/null and b/docs/images/searchAlexDavidResult.png differ diff --git a/docs/images/sociallyineptweeb.png b/docs/images/sociallyineptweeb.png new file mode 100644 index 00000000000..2f83e0a830a Binary files /dev/null and b/docs/images/sociallyineptweeb.png differ diff --git a/docs/images/themintchoco.png b/docs/images/themintchoco.png new file mode 100644 index 00000000000..072d8c818cb Binary files /dev/null and b/docs/images/themintchoco.png differ diff --git a/docs/images/zhannyhong.png b/docs/images/zhannyhong.png new file mode 100644 index 00000000000..dd48fd5c4d9 Binary files /dev/null and b/docs/images/zhannyhong.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..2b140744e73 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,20 @@ --- layout: page -title: AddressBook Level-3 +title: Tutorly --- -[![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) +[![Java CI](https://github.com/AY2425S2-CS2103T-T17-3/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2425S2-CS2103T-T17-3/tp/actions/workflows/gradle.yml) +[![codecov](https://codecov.io/gh/AY2425S2-CS2103T-T17-3/tp/graph/badge.svg?token=XI1D0BSATZ)](https://codecov.io/gh/AY2425S2-CS2103T-T17-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). +**Tutorly is a desktop application designed specifically for private tutors.** It empowers tutors to quickly track student records, log lesson details, record attendance, and generate progress reports - all through fast, keyboard-driven interactions. 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 Tutorly, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing Tutorly, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** +* This project is based on the [AddressBook-Level3](https://github.com/se-edu/addressbook-level3) project created by the SE-EDU initiative. * Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java deleted file mode 100644 index 92cd8fa605a..00000000000 --- a/src/main/java/seedu/address/logic/Logic.java +++ /dev/null @@ -1,50 +0,0 @@ -package seedu.address.logic; - -import java.nio.file.Path; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * API of the Logic component - */ -public interface Logic { - /** - * Executes the command and returns the result. - * @param commandText The command as entered by the user. - * @return the result of the command execution. - * @throws CommandException If an error occurs during command execution. - * @throws ParseException If an error occurs during parsing. - */ - CommandResult execute(String commandText) throws CommandException, ParseException; - - /** - * Returns the AddressBook. - * - * @see seedu.address.model.Model#getAddressBook() - */ - ReadOnlyAddressBook getAddressBook(); - - /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); - - /** - * Returns the user prefs' address book file path. - */ - Path getAddressBookFilePath(); - - /** - * Returns the user prefs' GUI settings. - */ - GuiSettings getGuiSettings(); - - /** - * Set the user prefs' GUI settings. - */ - void setGuiSettings(GuiSettings guiSettings); -} diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java deleted file mode 100644 index ecd32c31b53..00000000000 --- a/src/main/java/seedu/address/logic/Messages.java +++ /dev/null @@ -1,51 +0,0 @@ -package seedu.address.logic; - -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import seedu.address.logic.parser.Prefix; -import seedu.address.model.person.Person; - -/** - * Container for user visible messages. - */ -public class Messages { - - public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; - public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; - public static final String MESSAGE_DUPLICATE_FIELDS = - "Multiple values specified for the following single-valued field(s): "; - - /** - * Returns an error message indicating the duplicate prefixes. - */ - public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePrefixes) { - assert duplicatePrefixes.length > 0; - - Set duplicateFields = - Stream.of(duplicatePrefixes).map(Prefix::toString).collect(Collectors.toSet()); - - return MESSAGE_DUPLICATE_FIELDS + String.join(" ", duplicateFields); - } - - /** - * Formats the {@code person} for display to the user. - */ - public static String format(Person person) { - final StringBuilder builder = new StringBuilder(); - builder.append(person.getName()) - .append("; Phone: ") - .append(person.getPhone()) - .append("; Email: ") - .append(person.getEmail()) - .append("; Address: ") - .append(person.getAddress()) - .append("; Tags: "); - person.getTags().forEach(builder::append); - 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 deleted file mode 100644 index 5d7185a9680..00000000000 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ /dev/null @@ -1,84 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import 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; - -/** - * Adds a person to the address book. - */ -public class AddCommand extends Command { - - public static final String COMMAND_WORD = "add"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; - - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; - - private final Person toAdd; - - /** - * Creates an AddCommand to add the specified {@code Person} - */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof AddCommand)) { - return false; - } - - AddCommand otherAddCommand = (AddCommand) other; - return toAdd.equals(otherAddCommand.toAdd); - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .add("toAdd", toAdd) - .toString(); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java deleted file mode 100644 index 9c86b1fa6e4..00000000000 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ /dev/null @@ -1,23 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.model.AddressBook; -import seedu.address.model.Model; - -/** - * Clears the address book. - */ -public class ClearCommand extends Command { - - public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.setAddressBook(new AddressBook()); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java deleted file mode 100644 index 249b6072d0d..00000000000 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ /dev/null @@ -1,82 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import java.util.Objects; - -import seedu.address.commons.util.ToStringBuilder; - -/** - * Represents the result of a command execution. - */ -public class CommandResult { - - private final String feedbackToUser; - - /** Help information should be shown to the user. */ - private final boolean showHelp; - - /** The application should exit. */ - private final boolean exit; - - /** - * Constructs a {@code CommandResult} with the specified fields. - */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { - this.feedbackToUser = requireNonNull(feedbackToUser); - this.showHelp = showHelp; - this.exit = exit; - } - - /** - * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, - * and other fields set to their default value. - */ - public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); - } - - public String getFeedbackToUser() { - return feedbackToUser; - } - - public boolean isShowHelp() { - return showHelp; - } - - public boolean isExit() { - return exit; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof CommandResult)) { - return false; - } - - CommandResult otherCommandResult = (CommandResult) other; - return feedbackToUser.equals(otherCommandResult.feedbackToUser) - && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; - } - - @Override - public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .add("feedbackToUser", feedbackToUser) - .add("showHelp", showHelp) - .add("exit", exit) - .toString(); - } - -} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java deleted file mode 100644 index 1135ac19b74..00000000000 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ /dev/null @@ -1,69 +0,0 @@ -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; - -/** - * Deletes a person identified using it's displayed index from the address book. - */ -public class DeleteCommand extends Command { - - public static final String COMMAND_WORD = "delete"; - - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - - private final Index targetIndex; - - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof DeleteCommand)) { - return false; - } - - DeleteCommand otherDeleteCommand = (DeleteCommand) other; - return targetIndex.equals(otherDeleteCommand.targetIndex); - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .add("targetIndex", targetIndex) - .toString(); - } -} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java deleted file mode 100644 index 72b9eddd3a7..00000000000 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ /dev/null @@ -1,58 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.util.ToStringBuilder; -import seedu.address.logic.Messages; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. - */ -public class FindCommand extends Command { - - public static final String COMMAND_WORD = "find"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; - - private final NameContainsKeywordsPredicate predicate; - - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; - } - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof FindCommand)) { - return false; - } - - FindCommand otherFindCommand = (FindCommand) other; - return predicate.equals(otherFindCommand.predicate); - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .add("predicate", predicate) - .toString(); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java deleted file mode 100644 index 84be6ad2596..00000000000 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ /dev/null @@ -1,24 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import seedu.address.model.Model; - -/** - * Lists all persons in the address book to the user. - */ -public class ListCommand extends Command { - - public static final String COMMAND_WORD = "list"; - - public static final String MESSAGE_SUCCESS = "Listed all persons"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java deleted file mode 100644 index 4ff1a97ed77..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ /dev/null @@ -1,61 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Set; -import java.util.stream.Stream; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new AddCommand object - */ -public class AddCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the AddCommand - * and returns an AddCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public AddCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); - } - - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java deleted file mode 100644 index 75b1a9bf119..00000000000 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ /dev/null @@ -1,15 +0,0 @@ -package seedu.address.logic.parser; - -/** - * Contains Command Line Interface (CLI) syntax definitions common to multiple commands - */ -public class CliSyntax { - - /* Prefix definitions */ - public static final Prefix PREFIX_NAME = new Prefix("n/"); - public static final Prefix PREFIX_PHONE = new Prefix("p/"); - public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); - public static final Prefix PREFIX_TAG = new Prefix("t/"); - -} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java deleted file mode 100644 index 3527fe76a3e..00000000000 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ /dev/null @@ -1,29 +0,0 @@ -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.DeleteCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses input arguments and creates a new DeleteCommand object - */ -public class DeleteCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public DeleteCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); - } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java deleted file mode 100644 index 2867bde857b..00000000000 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ /dev/null @@ -1,33 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import java.util.Arrays; - -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Parses input arguments and creates a new FindCommand object - */ -public class FindCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the FindCommand - * and returns a FindCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } - - String[] nameKeywords = trimmedArgs.split("\\s+"); - - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java deleted file mode 100644 index b117acb9c55..00000000000 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ /dev/null @@ -1,124 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods used for parsing strings in the various *Parser classes. - */ -public class ParserUtil { - - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; - - /** - * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be - * trimmed. - * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). - */ - public static Index parseIndex(String oneBasedIndex) throws ParseException { - String trimmedIndex = oneBasedIndex.trim(); - if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { - throw new ParseException(MESSAGE_INVALID_INDEX); - } - return Index.fromOneBased(Integer.parseInt(trimmedIndex)); - } - - /** - * Parses a {@code String name} into a {@code Name}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code name} is invalid. - */ - public static Name parseName(String name) throws ParseException { - requireNonNull(name); - String trimmedName = name.trim(); - if (!Name.isValidName(trimmedName)) { - throw new ParseException(Name.MESSAGE_CONSTRAINTS); - } - return new Name(trimmedName); - } - - /** - * Parses a {@code String phone} into a {@code Phone}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code phone} is invalid. - */ - public static Phone parsePhone(String phone) throws ParseException { - requireNonNull(phone); - String trimmedPhone = phone.trim(); - if (!Phone.isValidPhone(trimmedPhone)) { - throw new ParseException(Phone.MESSAGE_CONSTRAINTS); - } - return new Phone(trimmedPhone); - } - - /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code address} is invalid. - */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); - } - return new Address(trimmedAddress); - } - - /** - * Parses a {@code String email} into an {@code Email}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code email} is invalid. - */ - public static Email parseEmail(String email) throws ParseException { - requireNonNull(email); - String trimmedEmail = email.trim(); - if (!Email.isValidEmail(trimmedEmail)) { - throw new ParseException(Email.MESSAGE_CONSTRAINTS); - } - return new Email(trimmedEmail); - } - - /** - * Parses a {@code String tag} into a {@code Tag}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code tag} is invalid. - */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { - throw new ParseException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(trimmedTag); - } - - /** - * Parses {@code Collection tags} into a {@code Set}. - */ - public static Set parseTags(Collection tags) throws ParseException { - requireNonNull(tags); - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); - } - return tagSet; - } -} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java deleted file mode 100644 index 73397161e84..00000000000 --- a/src/main/java/seedu/address/model/AddressBook.java +++ /dev/null @@ -1,130 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import javafx.collections.ObservableList; -import seedu.address.commons.util.ToStringBuilder; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; - -/** - * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) - */ -public class AddressBook implements ReadOnlyAddressBook { - - private final UniquePersonList persons; - - /* - * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication - * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html - * - * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication - * among constructors. - */ - { - persons = new UniquePersonList(); - } - - public AddressBook() {} - - /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} - */ - public AddressBook(ReadOnlyAddressBook toBeCopied) { - this(); - resetData(toBeCopied); - } - - //// list overwrite operations - - /** - * 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); - } - - /** - * Resets the existing data of this {@code AddressBook} with {@code newData}. - */ - public void resetData(ReadOnlyAddressBook newData) { - requireNonNull(newData); - - setPersons(newData.getPersonList()); - } - - //// person-level operations - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(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); - } - - /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); - - persons.setPerson(target, editedPerson); - } - - /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. - */ - public void removePerson(Person key) { - persons.remove(key); - } - - //// util methods - - @Override - public String toString() { - return new ToStringBuilder(this) - .add("persons", persons) - .toString(); - } - - @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof AddressBook)) { - return false; - } - - AddressBook otherAddressBook = (AddressBook) other; - return persons.equals(otherAddressBook.persons); - } - - @Override - public int hashCode() { - return persons.hashCode(); - } -} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java deleted file mode 100644 index d54df471c1f..00000000000 --- a/src/main/java/seedu/address/model/Model.java +++ /dev/null @@ -1,87 +0,0 @@ -package seedu.address.model; - -import java.nio.file.Path; -import java.util.function.Predicate; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; - -/** - * The API of the Model component. - */ -public interface Model { - /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; - - /** - * Replaces user prefs data with the data in {@code userPrefs}. - */ - void setUserPrefs(ReadOnlyUserPrefs userPrefs); - - /** - * Returns the user prefs. - */ - ReadOnlyUserPrefs getUserPrefs(); - - /** - * Returns the user prefs' GUI settings. - */ - GuiSettings getGuiSettings(); - - /** - * Sets the user prefs' GUI settings. - */ - void setGuiSettings(GuiSettings guiSettings); - - /** - * Returns the user prefs' address book file path. - */ - Path getAddressBookFilePath(); - - /** - * Sets the user prefs' address book file path. - */ - void setAddressBookFilePath(Path addressBookFilePath); - - /** - * Replaces address book data with the data in {@code addressBook}. - */ - void setAddressBook(ReadOnlyAddressBook addressBook); - - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - boolean hasPerson(Person person); - - /** - * Deletes the given person. - * The person must exist in the address book. - */ - void deletePerson(Person target); - - /** - * Adds the given person. - * {@code person} must not already exist in the address book. - */ - void addPerson(Person person); - - /** - * Replaces the given person {@code target} with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - void setPerson(Person target, Person editedPerson); - - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); - - /** - * 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); -} diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java deleted file mode 100644 index 57bc563fde6..00000000000 --- a/src/main/java/seedu/address/model/ModelManager.java +++ /dev/null @@ -1,148 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.nio.file.Path; -import java.util.function.Predicate; -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import javafx.collections.transformation.FilteredList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; - -/** - * Represents the in-memory model of the address book data. - */ -public class ModelManager implements Model { - private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - - private final AddressBook addressBook; - private final UserPrefs userPrefs; - private final FilteredList filteredPersons; - - /** - * Initializes a ModelManager with the given addressBook and userPrefs. - */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { - requireAllNonNull(addressBook, userPrefs); - - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - - this.addressBook = new AddressBook(addressBook); - this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); - } - - public ModelManager() { - this(new AddressBook(), new UserPrefs()); - } - - //=========== UserPrefs ================================================================================== - - @Override - public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { - requireNonNull(userPrefs); - this.userPrefs.resetData(userPrefs); - } - - @Override - public ReadOnlyUserPrefs getUserPrefs() { - return userPrefs; - } - - @Override - public GuiSettings getGuiSettings() { - return userPrefs.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - requireNonNull(guiSettings); - userPrefs.setGuiSettings(guiSettings); - } - - @Override - public Path getAddressBookFilePath() { - return userPrefs.getAddressBookFilePath(); - } - - @Override - public void setAddressBookFilePath(Path addressBookFilePath) { - requireNonNull(addressBookFilePath); - userPrefs.setAddressBookFilePath(addressBookFilePath); - } - - //=========== AddressBook ================================================================================ - - @Override - public void setAddressBook(ReadOnlyAddressBook addressBook) { - this.addressBook.resetData(addressBook); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return addressBook; - } - - @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return addressBook.hasPerson(person); - } - - @Override - public void deletePerson(Person target) { - addressBook.removePerson(target); - } - - @Override - public void addPerson(Person person) { - addressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - } - - @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - addressBook.setPerson(target, editedPerson); - } - - //=========== Filtered Person List Accessors ============================================================= - - /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of - * {@code versionedAddressBook} - */ - @Override - public ObservableList getFilteredPersonList() { - return filteredPersons; - } - - @Override - public void updateFilteredPersonList(Predicate predicate) { - requireNonNull(predicate); - filteredPersons.setPredicate(predicate); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof ModelManager)) { - return false; - } - - ModelManager otherModelManager = (ModelManager) other; - return addressBook.equals(otherModelManager.addressBook) - && userPrefs.equals(otherModelManager.userPrefs) - && filteredPersons.equals(otherModelManager.filteredPersons); - } - -} diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java deleted file mode 100644 index 6ddc2cd9a29..00000000000 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ /dev/null @@ -1,17 +0,0 @@ -package seedu.address.model; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; - -/** - * Unmodifiable view of an address book - */ -public interface ReadOnlyAddressBook { - - /** - * Returns an unmodifiable view of the persons list. - * This list will not contain any duplicate persons. - */ - ObservableList getPersonList(); - -} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java deleted file mode 100644 index 62d19be2977..00000000000 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ /dev/null @@ -1,44 +0,0 @@ -package seedu.address.model.person; - -import java.util.List; -import java.util.function.Predicate; - -import seedu.address.commons.util.StringUtil; -import seedu.address.commons.util.ToStringBuilder; - -/** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. - */ -public class NameContainsKeywordsPredicate implements Predicate { - private final List keywords; - - public NameContainsKeywordsPredicate(List keywords) { - this.keywords = keywords; - } - - @Override - public boolean test(Person person) { - return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof NameContainsKeywordsPredicate)) { - return false; - } - - NameContainsKeywordsPredicate otherNameContainsKeywordsPredicate = (NameContainsKeywordsPredicate) other; - return keywords.equals(otherNameContainsKeywordsPredicate.keywords); - } - - @Override - public String toString() { - return new ToStringBuilder(this).add("keywords", keywords).toString(); - } -} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java deleted file mode 100644 index cc0a68d79f9..00000000000 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ /dev/null @@ -1,150 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Iterator; -import java.util.List; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import seedu.address.model.person.exceptions.DuplicatePersonException; -import seedu.address.model.person.exceptions.PersonNotFoundException; - -/** - * A list of persons that enforces uniqueness between its elements and does not allow nulls. - * A person is considered unique by comparing using {@code Person#isSamePerson(Person)}. As such, adding and updating of - * persons uses Person#isSamePerson(Person) for equality so as to ensure that the person being added or updated is - * unique in terms of identity in the UniquePersonList. However, the removal of a person uses Person#equals(Object) so - * as to ensure that the person with exactly the same fields will be removed. - * - * Supports a minimal set of list operations. - * - * @see Person#isSamePerson(Person) - */ -public class UniquePersonList implements Iterable { - - private final ObservableList internalList = FXCollections.observableArrayList(); - private final ObservableList internalUnmodifiableList = - FXCollections.unmodifiableObservableList(internalList); - - /** - * Returns true if the list contains an equivalent person as the given argument. - */ - public boolean contains(Person toCheck) { - requireNonNull(toCheck); - return internalList.stream().anyMatch(toCheck::isSamePerson); - } - - /** - * Adds a person to the list. - * The person must not already exist in the list. - */ - public void add(Person toAdd) { - requireNonNull(toAdd); - if (contains(toAdd)) { - throw new DuplicatePersonException(); - } - internalList.add(toAdd); - } - - /** - * Replaces the person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the list. - * The person identity of {@code editedPerson} must not be the same as another existing person in the list. - */ - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - int index = internalList.indexOf(target); - if (index == -1) { - throw new PersonNotFoundException(); - } - - if (!target.isSamePerson(editedPerson) && contains(editedPerson)) { - throw new DuplicatePersonException(); - } - - internalList.set(index, editedPerson); - } - - /** - * Removes the equivalent person from the list. - * The person must exist in the list. - */ - public void remove(Person toRemove) { - requireNonNull(toRemove); - if (!internalList.remove(toRemove)) { - throw new PersonNotFoundException(); - } - } - - public void setPersons(UniquePersonList replacement) { - requireNonNull(replacement); - internalList.setAll(replacement.internalList); - } - - /** - * Replaces the contents of this list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - requireAllNonNull(persons); - if (!personsAreUnique(persons)) { - throw new DuplicatePersonException(); - } - - internalList.setAll(persons); - } - - /** - * Returns the backing list as an unmodifiable {@code ObservableList}. - */ - public ObservableList asUnmodifiableObservableList() { - return internalUnmodifiableList; - } - - @Override - public Iterator iterator() { - return internalList.iterator(); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof UniquePersonList)) { - return false; - } - - UniquePersonList otherUniquePersonList = (UniquePersonList) other; - return internalList.equals(otherUniquePersonList.internalList); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } - - @Override - public String toString() { - return internalList.toString(); - } - - /** - * Returns true if {@code persons} contains only unique persons. - */ - private boolean personsAreUnique(List persons) { - for (int i = 0; i < persons.size() - 1; i++) { - for (int j = i + 1; j < persons.size(); j++) { - if (persons.get(i).isSamePerson(persons.get(j))) { - return false; - } - } - } - return true; - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java deleted file mode 100644 index d7290f59442..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ /dev/null @@ -1,11 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same - * identity). - */ -public class DuplicatePersonException extends RuntimeException { - public DuplicatePersonException() { - super("Operation would result in duplicate persons"); - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java deleted file mode 100644 index fa764426ca7..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ /dev/null @@ -1,6 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation is unable to find the specified person. - */ -public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java deleted file mode 100644 index 1806da4facf..00000000000 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.model.util; - -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods for populating {@code AddressBook} with sample data. - */ -public class SampleDataUtil { - public static Person[] getSamplePersons() { - return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) - }; - } - - public static ReadOnlyAddressBook getSampleAddressBook() { - AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); - } - return sampleAb; - } - - /** - * Returns a tag set containing the list of strings given. - */ - public static Set getTagSet(String... strings) { - return Arrays.stream(strings) - .map(Tag::new) - .collect(Collectors.toSet()); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java deleted file mode 100644 index 5efd834091d..00000000000 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * An Immutable AddressBook that is serializable to JSON format. - */ -@JsonRootName(value = "addressbook") -class JsonSerializableAddressBook { - - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; - - private final List persons = new ArrayList<>(); - - /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. - */ - @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { - this.persons.addAll(persons); - } - - /** - * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. - * - * @param source future changes to this will not affect the created {@code JsonSerializableAddressBook}. - */ - public JsonSerializableAddressBook(ReadOnlyAddressBook source) { - persons.addAll(source.getPersonList().stream().map(JsonAdaptedPerson::new).collect(Collectors.toList())); - } - - /** - * Converts this address book into the model's {@code AddressBook} object. - * - * @throws IllegalValueException if there were any data constraints violated. - */ - public AddressBook toModelType() throws IllegalValueException { - AddressBook addressBook = new AddressBook(); - for (JsonAdaptedPerson jsonAdaptedPerson : persons) { - Person person = jsonAdaptedPerson.toModelType(); - if (addressBook.hasPerson(person)) { - throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); - } - addressBook.addPerson(person); - } - return addressBook; - } - -} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java deleted file mode 100644 index 094c42cda82..00000000000 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ /dev/null @@ -1,59 +0,0 @@ -package seedu.address.ui; - -import java.util.Comparator; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.layout.FlowPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import seedu.address.model.person.Person; - -/** - * An UI component that displays information of a {@code Person}. - */ -public class PersonCard extends UiPart { - - private static final String FXML = "PersonListCard.fxml"; - - /** - * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. - * As a consequence, UI elements' variable names cannot be set to such keywords - * or an exception will be thrown by JavaFX during runtime. - * - * @see The issue on AddressBook level 4 - */ - - public final Person person; - - @FXML - private HBox cardPane; - @FXML - private Label name; - @FXML - private Label id; - @FXML - private Label phone; - @FXML - private Label address; - @FXML - private Label email; - @FXML - private FlowPane tags; - - /** - * Creates a {@code PersonCode} with the given {@code Person} and index to display. - */ - public PersonCard(Person person, int displayedIndex) { - super(FXML); - this.person = person; - id.setText(displayedIndex + ". "); - name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - person.getTags().stream() - .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); - } -} diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java deleted file mode 100644 index f4c501a897b..00000000000 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ /dev/null @@ -1,49 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.layout.Region; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; - -/** - * Panel containing the list of persons. - */ -public class PersonListPanel extends UiPart { - private static final String FXML = "PersonListPanel.fxml"; - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - - @FXML - private ListView personListView; - - /** - * Creates a {@code PersonListPanel} with the given {@code ObservableList}. - */ - public PersonListPanel(ObservableList personList) { - super(FXML); - personListView.setItems(personList); - personListView.setCellFactory(listView -> new PersonListViewCell()); - } - - /** - * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. - */ - class PersonListViewCell extends ListCell { - @Override - protected void updateItem(Person person, boolean empty) { - super.updateItem(person, empty); - - if (empty || person == null) { - setGraphic(null); - setText(null); - } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); - } - } - } - -} diff --git a/src/main/java/seedu/address/AppParameters.java b/src/main/java/tutorly/AppParameters.java similarity index 92% rename from src/main/java/seedu/address/AppParameters.java rename to src/main/java/tutorly/AppParameters.java index 3d603622d4e..e380810ba78 100644 --- a/src/main/java/seedu/address/AppParameters.java +++ b/src/main/java/tutorly/AppParameters.java @@ -1,4 +1,4 @@ -package seedu.address; +package tutorly; import java.nio.file.Path; import java.nio.file.Paths; @@ -7,9 +7,9 @@ import java.util.logging.Logger; import javafx.application.Application; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.FileUtil; -import seedu.address.commons.util.ToStringBuilder; +import tutorly.commons.core.LogsCenter; +import tutorly.commons.util.FileUtil; +import tutorly.commons.util.ToStringBuilder; /** * Represents the parsed command-line parameters given to the application. diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/tutorly/Main.java similarity index 96% rename from src/main/java/seedu/address/Main.java rename to src/main/java/tutorly/Main.java index 9461d6da769..b72c29b16de 100644 --- a/src/main/java/seedu/address/Main.java +++ b/src/main/java/tutorly/Main.java @@ -1,9 +1,9 @@ -package seedu.address; +package tutorly; import java.util.logging.Logger; import javafx.application.Application; -import seedu.address.commons.core.LogsCenter; +import tutorly.commons.core.LogsCenter; /** * The main entry point to the application. diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/tutorly/MainApp.java similarity index 85% rename from src/main/java/seedu/address/MainApp.java rename to src/main/java/tutorly/MainApp.java index 678ddc8c218..f49b5e91e2b 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/tutorly/MainApp.java @@ -1,4 +1,4 @@ -package seedu.address; +package tutorly; import java.io.IOException; import java.nio.file.Path; @@ -7,36 +7,36 @@ import javafx.application.Application; import javafx.stage.Stage; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.core.Version; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.commons.util.ConfigUtil; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; -import seedu.address.logic.LogicManager; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; -import seedu.address.model.util.SampleDataUtil; -import seedu.address.storage.AddressBookStorage; -import seedu.address.storage.JsonAddressBookStorage; -import seedu.address.storage.JsonUserPrefsStorage; -import seedu.address.storage.Storage; -import seedu.address.storage.StorageManager; -import seedu.address.storage.UserPrefsStorage; -import seedu.address.ui.Ui; -import seedu.address.ui.UiManager; +import tutorly.commons.core.Config; +import tutorly.commons.core.LogsCenter; +import tutorly.commons.core.Version; +import tutorly.commons.exceptions.DataLoadingException; +import tutorly.commons.util.ConfigUtil; +import tutorly.commons.util.StringUtil; +import tutorly.logic.Logic; +import tutorly.logic.LogicManager; +import tutorly.model.AddressBook; +import tutorly.model.Model; +import tutorly.model.ModelManager; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.ReadOnlyUserPrefs; +import tutorly.model.UserPrefs; +import tutorly.model.util.SampleDataUtil; +import tutorly.storage.AddressBookStorage; +import tutorly.storage.JsonAddressBookStorage; +import tutorly.storage.JsonUserPrefsStorage; +import tutorly.storage.Storage; +import tutorly.storage.StorageManager; +import tutorly.storage.UserPrefsStorage; +import tutorly.ui.Ui; +import tutorly.ui.UiManager; /** * Runs the application. */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(1, 5, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/tutorly/commons/core/Config.java similarity index 94% rename from src/main/java/seedu/address/commons/core/Config.java rename to src/main/java/tutorly/commons/core/Config.java index 485f85a5e05..2e388288c07 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/tutorly/commons/core/Config.java @@ -1,11 +1,11 @@ -package seedu.address.commons.core; +package tutorly.commons.core; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Objects; import java.util.logging.Level; -import seedu.address.commons.util.ToStringBuilder; +import tutorly.commons.util.ToStringBuilder; /** * Config values used by the app diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/tutorly/commons/core/GuiSettings.java similarity index 96% rename from src/main/java/seedu/address/commons/core/GuiSettings.java rename to src/main/java/tutorly/commons/core/GuiSettings.java index a97a86ee8d7..c6d635a68c5 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/tutorly/commons/core/GuiSettings.java @@ -1,10 +1,10 @@ -package seedu.address.commons.core; +package tutorly.commons.core; import java.awt.Point; import java.io.Serializable; import java.util.Objects; -import seedu.address.commons.util.ToStringBuilder; +import tutorly.commons.util.ToStringBuilder; /** * A Serializable class that contains the GUI settings. diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/tutorly/commons/core/LogsCenter.java similarity index 99% rename from src/main/java/seedu/address/commons/core/LogsCenter.java rename to src/main/java/tutorly/commons/core/LogsCenter.java index 8cf8e15a0f0..2ec1b34dd8a 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/tutorly/commons/core/LogsCenter.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package tutorly.commons.core; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/tutorly/commons/core/Version.java similarity index 98% rename from src/main/java/seedu/address/commons/core/Version.java rename to src/main/java/tutorly/commons/core/Version.java index 491d24559b4..d33989d49b6 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/tutorly/commons/core/Version.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package tutorly.commons.core; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/tutorly/commons/core/index/Index.java similarity index 95% rename from src/main/java/seedu/address/commons/core/index/Index.java rename to src/main/java/tutorly/commons/core/index/Index.java index dd170d8b68d..d8681e91645 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/tutorly/commons/core/index/Index.java @@ -1,6 +1,6 @@ -package seedu.address.commons.core.index; +package tutorly.commons.core.index; -import seedu.address.commons.util.ToStringBuilder; +import tutorly.commons.util.ToStringBuilder; /** * Represents a zero-based or one-based index. diff --git a/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java b/src/main/java/tutorly/commons/exceptions/DataLoadingException.java similarity index 82% rename from src/main/java/seedu/address/commons/exceptions/DataLoadingException.java rename to src/main/java/tutorly/commons/exceptions/DataLoadingException.java index 9904ba47afe..a6926dc356d 100644 --- a/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java +++ b/src/main/java/tutorly/commons/exceptions/DataLoadingException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package tutorly.commons.exceptions; /** * Represents an error during loading of data from a file. diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java b/src/main/java/tutorly/commons/exceptions/IllegalValueException.java similarity index 93% rename from src/main/java/seedu/address/commons/exceptions/IllegalValueException.java rename to src/main/java/tutorly/commons/exceptions/IllegalValueException.java index 19124db485c..e6394cff92a 100644 --- a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java +++ b/src/main/java/tutorly/commons/exceptions/IllegalValueException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package tutorly.commons.exceptions; /** * Signals that some given data does not fulfill some constraints. diff --git a/src/main/java/seedu/address/commons/util/AppUtil.java b/src/main/java/tutorly/commons/util/AppUtil.java similarity index 94% rename from src/main/java/seedu/address/commons/util/AppUtil.java rename to src/main/java/tutorly/commons/util/AppUtil.java index 87aa89c0326..59bcddeb4c8 100644 --- a/src/main/java/seedu/address/commons/util/AppUtil.java +++ b/src/main/java/tutorly/commons/util/AppUtil.java @@ -1,9 +1,9 @@ -package seedu.address.commons.util; +package tutorly.commons.util; import static java.util.Objects.requireNonNull; import javafx.scene.image.Image; -import seedu.address.MainApp; +import tutorly.MainApp; /** * A container for App specific utility functions diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/tutorly/commons/util/CollectionUtil.java similarity index 96% rename from src/main/java/seedu/address/commons/util/CollectionUtil.java rename to src/main/java/tutorly/commons/util/CollectionUtil.java index eafe4dfd681..7cdac9459c4 100644 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ b/src/main/java/tutorly/commons/util/CollectionUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package tutorly.commons.util; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/commons/util/ConfigUtil.java b/src/main/java/tutorly/commons/util/ConfigUtil.java similarity index 77% rename from src/main/java/seedu/address/commons/util/ConfigUtil.java rename to src/main/java/tutorly/commons/util/ConfigUtil.java index 7b829c3c4cc..3824b83d16f 100644 --- a/src/main/java/seedu/address/commons/util/ConfigUtil.java +++ b/src/main/java/tutorly/commons/util/ConfigUtil.java @@ -1,11 +1,11 @@ -package seedu.address.commons.util; +package tutorly.commons.util; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.core.Config; -import seedu.address.commons.exceptions.DataLoadingException; +import tutorly.commons.core.Config; +import tutorly.commons.exceptions.DataLoadingException; /** * A class for accessing the Config File. diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/tutorly/commons/util/FileUtil.java similarity index 98% rename from src/main/java/seedu/address/commons/util/FileUtil.java rename to src/main/java/tutorly/commons/util/FileUtil.java index b1e2767cdd9..46721e71b19 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/tutorly/commons/util/FileUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package tutorly.commons.util; import java.io.IOException; import java.nio.file.Files; diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/tutorly/commons/util/JsonUtil.java similarity index 97% rename from src/main/java/seedu/address/commons/util/JsonUtil.java rename to src/main/java/tutorly/commons/util/JsonUtil.java index 100cb16c395..d2f1a3975ec 100644 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ b/src/main/java/tutorly/commons/util/JsonUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package tutorly.commons.util; import static java.util.Objects.requireNonNull; @@ -20,8 +20,8 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataLoadingException; +import tutorly.commons.core.LogsCenter; +import tutorly.commons.exceptions.DataLoadingException; /** * Converts a Java object instance to JSON and vice versa diff --git a/src/main/java/tutorly/commons/util/ObservableListUtil.java b/src/main/java/tutorly/commons/util/ObservableListUtil.java new file mode 100644 index 00000000000..a114abc37ca --- /dev/null +++ b/src/main/java/tutorly/commons/util/ObservableListUtil.java @@ -0,0 +1,48 @@ +package tutorly.commons.util; + +import java.util.List; +import java.util.function.Predicate; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; + +/** + * Utility class for observable lists. + */ +public class ObservableListUtil { + + /** + * Creates an observable list backed by an {@code ArrayList}. + */ + public static ObservableList arrayList() { + return FXCollections.observableArrayList(); + } + + /** + * Returns an unmodifiable view of the specified observable list. + */ + public static ObservableList unmodifiableList(ObservableList list) { + return FXCollections.unmodifiableObservableList(list); + } + + /** + * Creates a filtered list that is updated whenever the list or the dependencies change. + */ + public static FilteredList filteredList(ObservableList list, Predicate predicate, + List> dependencies) { + FilteredList filteredList = new FilteredList<>(list, predicate); + + for (ObservableList dependency : dependencies) { + dependency.addListener((ListChangeListener) change -> { + // Reset the predicate to refilter the list + filteredList.setPredicate(t -> false); + filteredList.setPredicate(predicate); + }); + } + + return filteredList; + } + +} diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/tutorly/commons/util/StringUtil.java similarity index 54% rename from src/main/java/seedu/address/commons/util/StringUtil.java rename to src/main/java/tutorly/commons/util/StringUtil.java index 61cc8c9a1cb..37555796577 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/tutorly/commons/util/StringUtil.java @@ -1,7 +1,7 @@ -package seedu.address.commons.util; +package tutorly.commons.util; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static tutorly.commons.util.AppUtil.checkArgument; import java.io.PrintWriter; import java.io.StringWriter; @@ -13,29 +13,30 @@ public class StringUtil { /** - * Returns true if the {@code sentence} contains the {@code word}. - * Ignores case, but a full word match is required. + * Returns true if {@code keyword} is a substring of any word in {@code sentence}. + * Ignores case and a partial match within a word is allowed. *
examples:
      *       containsWordIgnoreCase("ABc def", "abc") == true
-     *       containsWordIgnoreCase("ABc def", "DEF") == true
-     *       containsWordIgnoreCase("ABc def", "AB") == false //not a full word match
+     *       containsWordIgnoreCase("ABc def", "DE") == true // partial match allowed
+     *       containsWordIgnoreCase("ABc def", "ABcd") == false
      *       
* @param sentence cannot be null - * @param word cannot be null, cannot be empty, must be a single word + * @param keyword cannot be null, cannot be empty, must be a single word */ - public static boolean containsWordIgnoreCase(String sentence, String word) { + public static boolean containsWordIgnoreCase(String sentence, String keyword) { requireNonNull(sentence); - requireNonNull(word); + requireNonNull(keyword); - String preppedWord = word.trim(); - checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); - checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word"); + String preppedKeyword = keyword.trim(); + checkArgument(!preppedKeyword.isEmpty(), "Keyword parameter cannot be empty"); + checkArgument( + preppedKeyword.split("\\s+").length == 1, + "Keyword parameter should be a single word"); - String preppedSentence = sentence; - String[] wordsInPreppedSentence = preppedSentence.split("\\s+"); + String[] wordsInSentence = sentence.split("\\s+"); - return Arrays.stream(wordsInPreppedSentence) - .anyMatch(preppedWord::equalsIgnoreCase); + return Arrays.stream(wordsInSentence) + .anyMatch(w -> w.toLowerCase().contains(preppedKeyword.toLowerCase())); } /** @@ -49,13 +50,13 @@ public static String getDetails(Throwable t) { } /** - * Returns true if {@code s} represents a non-zero unsigned integer + * Returns true if {@code s} represents a non-zero unsigned integer, that can be parsed into an integer. * e.g. 1, 2, 3, ..., {@code Integer.MAX_VALUE}
* Will return false for any other non-null string input * e.g. empty string, "-1", "0", "+1", and " 2 " (untrimmed), "3 0" (contains whitespace), "1 a" (contains letters) * @throws NullPointerException if {@code s} is null. */ - public static boolean isNonZeroUnsignedInteger(String s) { + public static boolean isParsableNonZeroUnsignedInteger(String s) { requireNonNull(s); try { @@ -65,4 +66,13 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + /** + * Returns true if {@code s} represents a non-zero unsigned integer. + */ + public static boolean isNonZeroUnsignedInteger(String s) { + requireNonNull(s); + return s.matches("[1-9]\\d*"); + } + } diff --git a/src/main/java/seedu/address/commons/util/ToStringBuilder.java b/src/main/java/tutorly/commons/util/ToStringBuilder.java similarity index 97% rename from src/main/java/seedu/address/commons/util/ToStringBuilder.java rename to src/main/java/tutorly/commons/util/ToStringBuilder.java index d979b926734..f6745b78d9e 100644 --- a/src/main/java/seedu/address/commons/util/ToStringBuilder.java +++ b/src/main/java/tutorly/commons/util/ToStringBuilder.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package tutorly.commons.util; /** * Builds a string representation of an object that is suitable as the return value of {@link Object#toString()}. diff --git a/src/main/java/tutorly/logic/Logic.java b/src/main/java/tutorly/logic/Logic.java new file mode 100644 index 00000000000..cc6806e42e7 --- /dev/null +++ b/src/main/java/tutorly/logic/Logic.java @@ -0,0 +1,74 @@ +package tutorly.logic; + +import java.nio.file.Path; + +import javafx.collections.ObservableList; +import tutorly.commons.core.GuiSettings; +import tutorly.logic.commands.Command; +import tutorly.logic.commands.CommandResult; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.Model; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.person.Person; +import tutorly.model.session.Session; + +/** + * API of the Logic component + */ +public interface Logic { + /** + * Executes the command and returns the result. + * @param commandText The command as entered by the user. + * @return the result of the command execution. + * @throws CommandException If an error occurs during command execution. + * @throws ParseException If an error occurs during parsing. + */ + CommandResult execute(String commandText) throws CommandException, ParseException; + + /** + * Executes the command and returns the result. + * @param command The command to be executed. + * @return the result of the command execution. + * @throws CommandException If an error occurs during command execution. + */ + CommandResult execute(Command command) throws CommandException; + + /** + * Returns the AddressBook. + * + * @see Model#getAddressBook() + */ + ReadOnlyAddressBook getAddressBook(); + + /** Returns an unmodifiable view of the list of persons */ + ObservableList getPersonList(); + + /** Returns an unmodifiable view of the filtered list of persons */ + ObservableList getFilteredPersonList(); + + /** Returns an unmodifiable view of the list of sessions */ + ObservableList getSessionList(); + + /** Returns an unmodifiable view of the filtered list of sessions */ + ObservableList getFilteredSessionList(); + + /** Returns an unmodifiable view of the list of attendance records */ + ObservableList getAttendanceRecordList(); + + /** + * Returns the user prefs' address book file path. + */ + Path getAddressBookFilePath(); + + /** + * Returns the user prefs' GUI settings. + */ + GuiSettings getGuiSettings(); + + /** + * Set the user prefs' GUI settings. + */ + void setGuiSettings(GuiSettings guiSettings); +} diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/tutorly/logic/LogicManager.java similarity index 50% rename from src/main/java/seedu/address/logic/LogicManager.java rename to src/main/java/tutorly/logic/LogicManager.java index 5aa3b91c7d0..3ba26ae0922 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/tutorly/logic/LogicManager.java @@ -1,22 +1,25 @@ -package seedu.address.logic; +package tutorly.logic; import java.io.IOException; import java.nio.file.AccessDeniedException; import java.nio.file.Path; +import java.util.Stack; import java.util.logging.Logger; import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.Model; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; -import seedu.address.storage.Storage; +import tutorly.commons.core.GuiSettings; +import tutorly.commons.core.LogsCenter; +import tutorly.logic.commands.Command; +import tutorly.logic.commands.CommandResult; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.logic.parser.AddressBookParser; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.Model; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.person.Person; +import tutorly.model.session.Session; +import tutorly.storage.Storage; /** * The main LogicManager of the app. @@ -27,12 +30,16 @@ public class LogicManager implements Logic { public static final String FILE_OPS_PERMISSION_ERROR_FORMAT = "Could not save data to file %s due to insufficient permissions to write to the file or the folder."; + public static final String UNDO_STACK_EMPTY = "No command to undo."; + private final Logger logger = LogsCenter.getLogger(LogicManager.class); private final Model model; private final Storage storage; private final AddressBookParser addressBookParser; + private final Stack undoStack = new Stack<>(); + /** * Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}. */ @@ -46,9 +53,30 @@ public LogicManager(Model model, Storage storage) { public CommandResult execute(String commandText) throws CommandException, ParseException { logger.info("----------------[USER COMMAND][" + commandText + "]"); - CommandResult commandResult; - Command command = addressBookParser.parseCommand(commandText); - commandResult = command.execute(model); + Command command = addressBookParser.parse(commandText); + return execute(command); + } + + @Override + public CommandResult execute(Command command) throws CommandException { + CommandResult commandResult = command.execute(model); + + if (commandResult.hasReverseCommand()) { + Command reverseCommand = commandResult.getReverseCommand(); + undoStack.push(reverseCommand); + } + + if (commandResult.shouldReverseLast()) { + if (undoStack.isEmpty()) { + throw new CommandException(UNDO_STACK_EMPTY); + } + Command lastCommand = undoStack.pop(); + CommandResult undoCommandResult = lastCommand.execute(model); + + commandResult = new CommandResult.Builder(undoCommandResult) + .withFeedback(commandResult.getFeedbackToUser() + "\n" + undoCommandResult.getFeedbackToUser()) + .build(); + } try { storage.saveAddressBook(model.getAddressBook()); @@ -66,11 +94,31 @@ public ReadOnlyAddressBook getAddressBook() { return model.getAddressBook(); } + @Override + public ObservableList getPersonList() { + return model.getPersonList(); + } + @Override public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getSessionList() { + return model.getSessionList(); + } + + @Override + public ObservableList getFilteredSessionList() { + return model.getFilteredSessionList(); + } + + @Override + public ObservableList getAttendanceRecordList() { + return model.getAttendanceRecordList(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/tutorly/logic/Messages.java b/src/main/java/tutorly/logic/Messages.java new file mode 100644 index 00000000000..a98ca41fca5 --- /dev/null +++ b/src/main/java/tutorly/logic/Messages.java @@ -0,0 +1,98 @@ +package tutorly.logic; + +import static tutorly.logic.parser.ParserUtil.DATE_FORMATTER; +import static tutorly.logic.parser.ParserUtil.TIME_FORMATTER; + +import java.time.LocalDateTime; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import tutorly.logic.parser.Prefix; +import tutorly.model.person.Person; +import tutorly.model.session.Session; +import tutorly.model.session.Timeslot; + +/** + * Container for user visible messages. + */ +public class Messages { + + public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; + public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; + public static final String MESSAGE_PERSON_NOT_FOUND = "Student not found!"; + public static final String MESSAGE_SESSION_NOT_FOUND = "Session not found!"; + public static final String MESSAGE_DUPLICATE_PERSON = "This student already exists."; + public static final String MESSAGE_SESSION_OVERLAP = "This session overlaps with another session."; + public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d students listed!"; + public static final String MESSAGE_SESSIONS_LISTED_OVERVIEW = "%1$d sessions listed!"; + public static final String MESSAGE_PERSONS_SHOWN = "Showing students"; + public static final String MESSAGE_PERSON_SHOWN = "Showing student %1$s"; + public static final String MESSAGE_SESSIONS_SHOWN = "Showing sessions"; + public static final String MESSAGE_SESSION_SHOWN = "Showing session %1$s"; + public static final String MESSAGE_DUPLICATE_FIELDS = + "Multiple values specified for the following single-valued field(s): "; + + /** + * Returns an error message indicating the duplicate prefixes. + */ + public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePrefixes) { + assert duplicatePrefixes.length > 0; + + Set duplicateFields = + Stream.of(duplicatePrefixes).map(Prefix::toString).collect(Collectors.toSet()); + + return MESSAGE_DUPLICATE_FIELDS + String.join(" ", duplicateFields); + } + + /** + * Formats the {@code person} for display to the user. + */ + public static String format(Person person) { + final StringBuilder builder = new StringBuilder(); + builder.append("Id: ") + .append(person.getId()) + .append("; Name: ") + .append(person.getName()) + .append("; Phone: ") + .append(person.getPhone()) + .append("; Email: ") + .append(person.getEmail()) + .append("; Address: ") + .append(person.getAddress()) + .append("; Tags: ") + .append(person.getTags().stream().map(tag -> tag.tagName).collect(Collectors.toList())) + .append("; Memo: ") + .append(person.getMemo()); + return builder.toString(); + } + + /** + * Formats the {@code timeslot} for display to the user. + */ + public static String format(Timeslot timeslot) { + LocalDateTime start = timeslot.getStartTime(); + LocalDateTime end = timeslot.getEndTime(); + if (start.toLocalDate().equals(end.toLocalDate())) { + return String.format("%s %s - %s", start.format(DATE_FORMATTER), start.format(TIME_FORMATTER), + end.format(TIME_FORMATTER)); + } + + return String.format("%s %s - %s %s", start.format(DATE_FORMATTER), start.format(TIME_FORMATTER), + end.format(DATE_FORMATTER), end.format(TIME_FORMATTER)); + } + + /** + * Formats the {@code session} for display to the user. + */ + public static String format(Session session) { + final StringBuilder builder = new StringBuilder(); + builder.append("Id: ") + .append(session.getId()) + .append("; Timeslot: ") + .append(format(session.getTimeslot())) + .append("; Subject: ") + .append(session.getSubject()); + return builder.toString(); + } +} diff --git a/src/main/java/tutorly/logic/commands/AddSessionCommand.java b/src/main/java/tutorly/logic/commands/AddSessionCommand.java new file mode 100644 index 00000000000..f3b2cead721 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/AddSessionCommand.java @@ -0,0 +1,90 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.logic.parser.CliSyntax.PREFIX_SUBJECT; +import static tutorly.logic.parser.CliSyntax.PREFIX_TIMESLOT; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.session.Session; +import tutorly.ui.Tab; + +/** + * Creates a new tutoring session. + */ +public class AddSessionCommand extends SessionCommand { + + public static final String COMMAND_WORD = "add"; + public static final String COMMAND_STRING = SessionCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Creates a tutoring session." + + "\nParameters: " + + PREFIX_TIMESLOT + "TIMESLOT " + + PREFIX_SUBJECT + "SUBJECT" + + "\nExample: " + COMMAND_STRING + " " + + PREFIX_TIMESLOT + "30 Mar 2025 11:30-13:30 " + + PREFIX_SUBJECT + "Mathematics"; + + public static final String MESSAGE_SUCCESS = "New session created: %1$s"; + public static final String MESSAGE_DUPLICATE_SESSION = "This session already exists."; + public static final String MESSAGE_LIMIT_REACHED = "Limit reached; cannot add any more sessions. " + + "Use the clear command to reset."; + + private final Session toCreate; + + /** + * Creates a CreateSessionCommand to add the specified {@code Session}. + * + * @param session The session to be created. + */ + public AddSessionCommand(Session session) { + requireNonNull(session); + toCreate = session; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasOverlappingSession(toCreate)) { + throw new CommandException(Messages.MESSAGE_SESSION_OVERLAP); + } else if (model.hasSession(toCreate)) { + throw new CommandException(MESSAGE_DUPLICATE_SESSION); + } + + try { + model.addSession(toCreate); + } catch (IllegalStateException e) { + throw new CommandException(MESSAGE_LIMIT_REACHED); + } + + model.updateFilteredSessionList(Model.FILTER_SHOW_ALL_SESSIONS); + return new CommandResult.Builder(String.format(MESSAGE_SUCCESS, Messages.format(toCreate))) + .withTab(Tab.session(toCreate)) + .withReverseCommand(new DeleteSessionCommand(toCreate.getId())) + .build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AddSessionCommand otherCommand)) { + return false; + } + + return toCreate.equals(otherCommand.toCreate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("toCreate", toCreate) + .toString(); + } +} diff --git a/src/main/java/tutorly/logic/commands/AddStudentCommand.java b/src/main/java/tutorly/logic/commands/AddStudentCommand.java new file mode 100644 index 00000000000..8b570858d7d --- /dev/null +++ b/src/main/java/tutorly/logic/commands/AddStudentCommand.java @@ -0,0 +1,100 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static tutorly.logic.parser.CliSyntax.PREFIX_EMAIL; +import static tutorly.logic.parser.CliSyntax.PREFIX_MEMO; +import static tutorly.logic.parser.CliSyntax.PREFIX_NAME; +import static tutorly.logic.parser.CliSyntax.PREFIX_PHONE; +import static tutorly.logic.parser.CliSyntax.PREFIX_TAG; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.person.Identity; +import tutorly.model.person.Person; +import tutorly.ui.Tab; + +/** + * Adds a person to the address book. + */ +public class AddStudentCommand extends StudentCommand { + + public static final String COMMAND_WORD = "add"; + public static final String COMMAND_STRING = StudentCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Adds a student to the app." + + "\nParameters: " + + PREFIX_NAME + "NAME " + + "[" + PREFIX_PHONE + "PHONE] " + + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_TAG + "TAG]... " + + "[" + PREFIX_MEMO + "MEMO]" + + "\nExample: " + COMMAND_STRING + " " + + PREFIX_NAME + "John Doe " + + PREFIX_PHONE + "98765432 " + + PREFIX_EMAIL + "johnd@example.com " + + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " + + PREFIX_TAG + "friends " + + PREFIX_TAG + "owesMoney " + + PREFIX_MEMO + "Needs extra help in understanding OOP"; + + public static final String MESSAGE_SUCCESS = "New student added: %1$s"; + public static final String MESSAGE_LIMIT_REACHED = "Limit reached; cannot add any more students. " + + "Use the clear command to reset."; + + private final Person toAdd; + + /** + * Creates an AddCommand to add the specified {@code Person} + */ + public AddStudentCommand(Person person) { + requireNonNull(person); + toAdd = person; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasPerson(toAdd)) { + throw new CommandException(Messages.MESSAGE_DUPLICATE_PERSON); + } + + try { + model.addPerson(toAdd); + } catch (IllegalStateException e) { + throw new CommandException(MESSAGE_LIMIT_REACHED); + } + + model.updateFilteredPersonList(Model.FILTER_SHOW_ALL_PERSONS); + return new CommandResult.Builder(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))) + .withTab(Tab.student(toAdd)) + .withReverseCommand(new DeleteStudentCommand(new Identity(toAdd.getId()))) + .build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddStudentCommand otherAddCommand)) { + return false; + } + + return toAdd.equals(otherAddCommand.toAdd); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("toAdd", toAdd) + .toString(); + } +} diff --git a/src/main/java/tutorly/logic/commands/AttendanceFeedbackCommand.java b/src/main/java/tutorly/logic/commands/AttendanceFeedbackCommand.java new file mode 100644 index 00000000000..34d2ea4ba1e --- /dev/null +++ b/src/main/java/tutorly/logic/commands/AttendanceFeedbackCommand.java @@ -0,0 +1,108 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.logic.parser.CliSyntax.PREFIX_FEEDBACK; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; + +import java.util.Optional; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.attendancerecord.Feedback; +import tutorly.model.person.Identity; +import tutorly.model.person.Person; +import tutorly.model.session.Session; +import tutorly.ui.Tab; + +/** + * Provides feedback for a student in a session. + */ +public class AttendanceFeedbackCommand extends SessionCommand { + + public static final String COMMAND_WORD = "feedback"; + public static final String COMMAND_STRING = SessionCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Provides feedback for a student in a session. " + + "\nParameters: STUDENT_IDENTIFIER " + + PREFIX_SESSION + "SESSION_ID " + + PREFIX_FEEDBACK + "FEEDBACK" + + "\nExample: " + COMMAND_STRING + " 1 " + + PREFIX_SESSION + "2 " + + PREFIX_FEEDBACK + "Good job!"; + + public static final String MESSAGE_SUCCESS = "Feedback updated for %1$s in Session: %2$s"; + public static final String MESSAGE_RECORD_NOT_FOUND = "%1$s is not assigned to Session: %2$s"; + + private final Identity identity; + private final int sessionId; + private final Feedback feedback; + + /** + * Creates an AttendanceFeedbackCommand for the given {@code identity} and {@code Session} + */ + public AttendanceFeedbackCommand(Identity identity, int sessionId, Feedback feedback) { + this.identity = requireNonNull(identity); + this.sessionId = sessionId; + this.feedback = requireNonNull(feedback); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Optional person = model.getPersonByIdentity(identity); + if (person.isEmpty()) { + throw new CommandException(Messages.MESSAGE_PERSON_NOT_FOUND); + } + + Optional session = model.getSessionById(sessionId); + if (session.isEmpty()) { + throw new CommandException(Messages.MESSAGE_SESSION_NOT_FOUND); + } + + AttendanceRecord dummyRecord = new AttendanceRecord(person.get().getId(), sessionId, true, Feedback.empty()); + Optional existingRecord = model.findAttendanceRecord(dummyRecord); + if (existingRecord.isEmpty()) { + throw new CommandException(String.format(MESSAGE_RECORD_NOT_FOUND, + person.get().getName().fullName, Messages.format(session.get()))); + } + + AttendanceRecord record = new AttendanceRecord(person.get().getId(), sessionId, + existingRecord.get().getAttendance(), feedback); + + model.setAttendanceRecord(existingRecord.get(), record); + model.updateFilteredSessionList(Model.FILTER_SHOW_ALL_SESSIONS); + return new CommandResult.Builder( + String.format(MESSAGE_SUCCESS, person.get().getName().fullName, Messages.format(session.get()))) + .withTab(Tab.session()) + .withReverseCommand(new AttendanceFeedbackCommand(identity, sessionId, + existingRecord.get().getFeedback())) + .build(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AttendanceFeedbackCommand otherCommand)) { + return false; + } + return identity.equals(otherCommand.identity) + && sessionId == otherCommand.sessionId + && feedback.equals(otherCommand.feedback); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("identity", identity) + .add("sessionId", sessionId) + .add("feedback", feedback) + .toString(); + } +} diff --git a/src/main/java/tutorly/logic/commands/AttendanceMarkSessionCommand.java b/src/main/java/tutorly/logic/commands/AttendanceMarkSessionCommand.java new file mode 100644 index 00000000000..cb4f6d334bc --- /dev/null +++ b/src/main/java/tutorly/logic/commands/AttendanceMarkSessionCommand.java @@ -0,0 +1,111 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; + +import java.util.Optional; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.attendancerecord.Feedback; +import tutorly.model.person.Identity; +import tutorly.model.person.Person; +import tutorly.model.session.Session; +import tutorly.ui.Tab; + +/** + * Marks attendance for a student in a session. + */ +public class AttendanceMarkSessionCommand extends SessionCommand { + + public static final String COMMAND_WORD = "mark"; + public static final String COMMAND_STRING = SessionCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Marks attendance for a student in a session." + + "\nParameters: STUDENT_IDENTIFIER " + + PREFIX_SESSION + "SESSION_ID" + + "\nExample: " + COMMAND_STRING + " 1 " + + PREFIX_SESSION + "2"; + + public static final String MESSAGE_SUCCESS = "Marked %1$s's for Session: %2$s"; + public static final String MESSAGE_RECORD_NOT_FOUND = "%1$s is not assigned to Session: %2$s"; + public static final String MESSAGE_ALREADY_MARKED = "%1$s's attendance is already marked for Session: %2$s"; + + private final Identity identity; + private final int sessionId; + + /** + * Creates an AttendanceMarkSessionCommand for the given {@code identity} and {@code Session} + */ + public AttendanceMarkSessionCommand(Identity identity, int sessionId) { + this.identity = requireNonNull(identity); + this.sessionId = sessionId; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Optional person = model.getPersonByIdentity(identity); + if (person.isEmpty()) { + throw new CommandException(Messages.MESSAGE_PERSON_NOT_FOUND); + } + + Optional session = model.getSessionById(sessionId); + if (session.isEmpty()) { + throw new CommandException(Messages.MESSAGE_SESSION_NOT_FOUND); + } + + AttendanceRecord dummyRecord = new AttendanceRecord(person.get().getId(), sessionId, + true, Feedback.empty()); + Optional existingRecord = model.findAttendanceRecord(dummyRecord); + if (existingRecord.isEmpty()) { + throw new CommandException(String.format(MESSAGE_RECORD_NOT_FOUND, + person.get().getName().fullName, Messages.format(session.get()))); + } + + if (existingRecord.get().getAttendance()) { + throw new CommandException(String.format(MESSAGE_ALREADY_MARKED, + person.get().getName().fullName, Messages.format(session.get()))); + } + + AttendanceRecord record = new AttendanceRecord(person.get().getId(), sessionId, + true, existingRecord.get().getFeedback()); + + model.setAttendanceRecord(existingRecord.get(), record); + model.updateFilteredSessionList(Model.FILTER_SHOW_ALL_SESSIONS); + return new CommandResult.Builder( + String.format(MESSAGE_SUCCESS, person.get().getName().fullName, Messages.format(session.get()))) + .withTab(Tab.attendanceRecord(session.get(), record)) + .withReverseCommand(new AttendanceUnmarkSessionCommand(identity, sessionId)) + .build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AttendanceMarkSessionCommand otherAttendanceMarkSessionCommand)) { + return false; + } + + return identity.equals(otherAttendanceMarkSessionCommand.identity) + && sessionId == otherAttendanceMarkSessionCommand.sessionId; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("identity", identity) + .add("sessionId", sessionId) + .toString(); + } + +} diff --git a/src/main/java/tutorly/logic/commands/AttendanceUnmarkSessionCommand.java b/src/main/java/tutorly/logic/commands/AttendanceUnmarkSessionCommand.java new file mode 100644 index 00000000000..8636dbb548d --- /dev/null +++ b/src/main/java/tutorly/logic/commands/AttendanceUnmarkSessionCommand.java @@ -0,0 +1,111 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; + +import java.util.Optional; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.attendancerecord.Feedback; +import tutorly.model.person.Identity; +import tutorly.model.person.Person; +import tutorly.model.session.Session; +import tutorly.ui.Tab; + +/** + * Unmarks attendance for a student in a session. + */ +public class AttendanceUnmarkSessionCommand extends SessionCommand { + + public static final String COMMAND_WORD = "unmark"; + public static final String COMMAND_STRING = SessionCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Unmarks attendance for a student in a session." + + "\nParameters: STUDENT_IDENTIFIER " + + PREFIX_SESSION + "SESSION_ID" + + "\nExample: " + COMMAND_STRING + " 1 " + + PREFIX_SESSION + "2"; + + public static final String MESSAGE_SUCCESS = "Unmarked %1$s's for Session: %2$s"; + public static final String MESSAGE_RECORD_NOT_FOUND = "%1$s is not assigned to Session: %2$s"; + public static final String MESSAGE_ALREADY_UNMARKED = "%1$s's attendance is not marked for Session: %2$s"; + + private final Identity identity; + private final int sessionId; + + /** + * Creates an AttendanceUnmarkSessionCommand for the given {@code identity} and {@code Session} + */ + public AttendanceUnmarkSessionCommand(Identity identity, int sessionId) { + this.identity = requireNonNull(identity); + this.sessionId = sessionId; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Optional person = model.getPersonByIdentity(identity); + if (person.isEmpty()) { + throw new CommandException(Messages.MESSAGE_PERSON_NOT_FOUND); + } + + Optional session = model.getSessionById(sessionId); + if (session.isEmpty()) { + throw new CommandException(Messages.MESSAGE_SESSION_NOT_FOUND); + } + + AttendanceRecord dummyRecord = new AttendanceRecord(person.get().getId(), sessionId, + false, Feedback.empty()); + Optional existingRecord = model.findAttendanceRecord(dummyRecord); + if (existingRecord.isEmpty()) { + throw new CommandException(String.format(MESSAGE_RECORD_NOT_FOUND, + person.get().getName().fullName, Messages.format(session.get()))); + } + + if (!existingRecord.get().getAttendance()) { + throw new CommandException(String.format(MESSAGE_ALREADY_UNMARKED, + person.get().getName().fullName, Messages.format(session.get()))); + } + + AttendanceRecord record = new AttendanceRecord(person.get().getId(), sessionId, + false, existingRecord.get().getFeedback()); + + model.setAttendanceRecord(existingRecord.get(), record); + model.updateFilteredSessionList(Model.FILTER_SHOW_ALL_SESSIONS); + return new CommandResult.Builder( + String.format(MESSAGE_SUCCESS, person.get().getName().fullName, Messages.format(session.get()))) + .withTab(Tab.attendanceRecord(session.get(), record)) + .withReverseCommand(new AttendanceMarkSessionCommand(identity, sessionId)) + .build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AttendanceUnmarkSessionCommand otherAttendanceUnmarkSessionCommand)) { + return false; + } + + return identity.equals(otherAttendanceUnmarkSessionCommand.identity) + && sessionId == otherAttendanceUnmarkSessionCommand.sessionId; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("identity", identity) + .add("sessionId", sessionId) + .toString(); + } + +} diff --git a/src/main/java/tutorly/logic/commands/ClearCommand.java b/src/main/java/tutorly/logic/commands/ClearCommand.java new file mode 100644 index 00000000000..08854dd8c24 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/ClearCommand.java @@ -0,0 +1,51 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.Optional; + +import tutorly.model.AddressBook; +import tutorly.model.Model; +import tutorly.model.ReadOnlyAddressBook; + +/** + * Clears the address book. + */ +public class ClearCommand extends Command { + + public static final String COMMAND_WORD = "clear"; + public static final String COMMAND_STRING = COMMAND_WORD; + + public static final String MESSAGE_CLEAR_SUCCESS = "Students, sessions and attendance records have been cleared!"; + public static final String MESSAGE_RESTORE_SUCCESS = + "Students, sessions and attendance records have been restored!"; + + private final Optional addressBook; + + /** + * Creates a ClearCommand to clear the address book to the specified {@code addressBook}. + */ + public ClearCommand(ReadOnlyAddressBook addressBook) { + this.addressBook = Optional.ofNullable(addressBook); + } + + /** + * Creates a ClearCommand to clear the address book to an empty state. + */ + public ClearCommand() { + this(null); + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + + ReadOnlyAddressBook currentAddressBook = new AddressBook(model.getAddressBook()); + model.setAddressBook(addressBook.orElseGet(() -> new AddressBook())); + + return new CommandResult.Builder(addressBook.isPresent() ? MESSAGE_RESTORE_SUCCESS : MESSAGE_CLEAR_SUCCESS) + .withReverseCommand(new ClearCommand(currentAddressBook)) + .build(); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/tutorly/logic/commands/Command.java similarity index 78% rename from src/main/java/seedu/address/logic/commands/Command.java rename to src/main/java/tutorly/logic/commands/Command.java index 64f18992160..9508580454a 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/tutorly/logic/commands/Command.java @@ -1,7 +1,7 @@ -package seedu.address.logic.commands; +package tutorly.logic.commands; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; /** * Represents a command with hidden internal logic and the ability to be executed. diff --git a/src/main/java/tutorly/logic/commands/CommandResult.java b/src/main/java/tutorly/logic/commands/CommandResult.java new file mode 100644 index 00000000000..2f377291049 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/CommandResult.java @@ -0,0 +1,248 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; +import java.util.Optional; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.ui.Tab; + +/** + * Represents the result of a command execution. + */ +public class CommandResult { + + /** Feedback displayed to the user */ + private final String feedbackToUser; + + /** Help information should be shown to the user. */ + private final boolean shouldShowHelp; + + /** The application should exit. */ + private final boolean shouldExit; + + /** The last reversible command should be reversed. */ + private final boolean shouldReverseLast; + + /** Tab that the user should be switched to. */ + private final Optional tab; + + /** The command that undoes the effects of this command. */ + private final Optional reverseCommand; + + /** + * Constructs a {@code CommandResult} with the specified fields. + */ + public CommandResult(String feedbackToUser, boolean shouldShowHelp, boolean shouldExit, boolean shouldReverseLast, + Tab tab, Command reverseCommand) { + requireNonNull(feedbackToUser); + + this.feedbackToUser = feedbackToUser; + this.shouldShowHelp = shouldShowHelp; + this.shouldExit = shouldExit; + this.shouldReverseLast = shouldReverseLast; + this.tab = Optional.ofNullable(tab); + this.reverseCommand = Optional.ofNullable(reverseCommand); + } + + /** + * Constructs a {@code CommandResult} with {@code feedbackToUser}, + * and default values for the other fields. + */ + public CommandResult(String feedbackToUser) { + this(feedbackToUser, false, false, false, null, null); + } + + /** + * Constructs a {@code CommandResult} with {@code feedbackToUser}, {@code shouldShowHelp}, {@code shouldExit}, + * and default values for the other fields. + */ + public CommandResult(String feedbackToUser, boolean shouldShowHelp, boolean shouldExit) { + this(feedbackToUser, shouldShowHelp, shouldExit, false, null, null); + } + + private CommandResult(Builder builder) { + this.feedbackToUser = builder.feedbackToUser; + this.shouldShowHelp = builder.shouldShowHelp; + this.shouldExit = builder.shouldExit; + this.shouldReverseLast = builder.shouldReverseLast; + this.tab = builder.tab; + this.reverseCommand = builder.reverseCommand; + } + + public String getFeedbackToUser() { + return feedbackToUser; + } + + public boolean shouldShowHelp() { + return shouldShowHelp; + } + + public boolean shouldExit() { + return shouldExit; + } + + public boolean shouldReverseLast() { + return shouldReverseLast; + } + + public Tab getTab() { + return tab.get(); + } + + public Command getReverseCommand() { + return reverseCommand.get(); + } + + public boolean shouldSwitchTab() { + return tab.isPresent(); + } + + public boolean hasReverseCommand() { + return reverseCommand.isPresent(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof CommandResult)) { + return false; + } + + CommandResult otherCommandResult = (CommandResult) other; + return feedbackToUser.equals(otherCommandResult.feedbackToUser) + && shouldShowHelp == otherCommandResult.shouldShowHelp + && shouldExit == otherCommandResult.shouldExit + && shouldReverseLast == otherCommandResult.shouldReverseLast + && tab.equals(otherCommandResult.tab) + && reverseCommand.equals(otherCommandResult.reverseCommand); + } + + @Override + public int hashCode() { + return Objects.hash(feedbackToUser, shouldShowHelp, shouldExit, shouldReverseLast, tab, reverseCommand); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("feedbackToUser", feedbackToUser) + .add("shouldShowHelp", shouldShowHelp) + .add("shouldExit", shouldExit) + .add("shouldReverseLast", shouldReverseLast) + .add("tab", tab.orElse(null)) + .add("reverseCommand", reverseCommand.orElse(null)) + .toString(); + } + + /** + * Builder for {@code CommandResult}. + */ + public static class Builder { + private String feedbackToUser; + private boolean shouldShowHelp; + private boolean shouldExit; + private boolean shouldReverseLast; + private Optional tab; + private Optional reverseCommand; + + /** + * Constructs a {@code CommandResult.Builder} with the specified feedback. + */ + public Builder(String feedbackToUser) { + this.feedbackToUser = feedbackToUser; + this.tab = Optional.empty(); + this.reverseCommand = Optional.empty(); + } + + /** + * Constructs a {@code CommandResult.Builder} from an existing {@code CommandResult}. + */ + public Builder(CommandResult commandResult) { + this.feedbackToUser = commandResult.getFeedbackToUser(); + this.shouldShowHelp = commandResult.shouldShowHelp(); + this.shouldExit = commandResult.shouldExit(); + this.shouldReverseLast = commandResult.shouldReverseLast(); + this.tab = commandResult.tab; + this.reverseCommand = commandResult.reverseCommand; + } + + /** + * Sets the feedback to the user. + */ + public Builder withFeedback(String feedbackToUser) { + this.feedbackToUser = feedbackToUser; + return this; + } + + /** + * Sets whether help information should be shown to the user. + */ + public Builder withShowHelp(boolean shouldShowHelp) { + this.shouldShowHelp = shouldShowHelp; + return this; + } + + /** + * Sets whether the application should exit. + */ + public Builder withExit(boolean shouldExit) { + this.shouldExit = shouldExit; + return this; + } + /** + * Sets whether the last reversible command should be reversed. + */ + public Builder withReverseLast(boolean shouldReverseLast) { + this.shouldReverseLast = shouldReverseLast; + return this; + } + + /** + * Sets the tab that the user should be switched to. + */ + public Builder withTab(Tab tab) { + this.tab = Optional.of(tab); + return this; + } + + /** + * Sets the reverse command. + */ + public Builder withReverseCommand(Command reverseCommand) { + this.reverseCommand = Optional.of(reverseCommand); + return this; + } + + /** + * Sets that help information should be shown to the user. + */ + public Builder showHelp() { + return withShowHelp(true); + } + + /** + * Sets that the application should exit. + */ + public Builder exit() { + return withExit(true); + } + + /** + * Sets that the last reversible command should be reversed. + */ + public Builder reverseLast() { + return withReverseLast(true); + } + + public CommandResult build() { + return new CommandResult(this); + } + } + +} diff --git a/src/main/java/tutorly/logic/commands/DeleteSessionCommand.java b/src/main/java/tutorly/logic/commands/DeleteSessionCommand.java new file mode 100644 index 00000000000..d583e462162 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/DeleteSessionCommand.java @@ -0,0 +1,72 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.Optional; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.session.Session; +import tutorly.ui.Tab; + +/** + * Deletes a session identified by its ID from the address book. + */ +public class DeleteSessionCommand extends SessionCommand { + + public static final String COMMAND_WORD = "delete"; + public static final String COMMAND_STRING = SessionCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Deletes the session identified by a SESSION_ID." + + "\nParameters: SESSION_ID" + + "\nExample: " + COMMAND_STRING + " 1"; + + public static final String MESSAGE_DELETE_SESSION_SUCCESS = "Deleted session: %1$s"; + + private final int sessionId; + + public DeleteSessionCommand(int sessionId) { + this.sessionId = sessionId; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Optional toDelete = model.getSessionById(sessionId); + if (toDelete.isEmpty()) { + throw new CommandException(Messages.MESSAGE_SESSION_NOT_FOUND); + } + + model.deleteSession(toDelete.get()); + model.updateFilteredSessionList(Model.FILTER_SHOW_ALL_SESSIONS); + return new CommandResult.Builder(String.format(MESSAGE_DELETE_SESSION_SUCCESS, Messages.format(toDelete.get()))) + .withTab(Tab.session()) + .withReverseCommand(new AddSessionCommand(toDelete.get())) + .build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof DeleteSessionCommand)) { + return false; + } + + DeleteSessionCommand otherCommand = (DeleteSessionCommand) other; + return sessionId == otherCommand.sessionId; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("sessionId", sessionId) + .toString(); + } +} diff --git a/src/main/java/tutorly/logic/commands/DeleteStudentCommand.java b/src/main/java/tutorly/logic/commands/DeleteStudentCommand.java new file mode 100644 index 00000000000..e3b35a50c84 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/DeleteStudentCommand.java @@ -0,0 +1,73 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.Optional; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.person.Identity; +import tutorly.model.person.Person; +import tutorly.ui.Tab; + +/** + * Deletes a person identified by their ID or name from the address book. + */ +public class DeleteStudentCommand extends StudentCommand { + + public static final String COMMAND_WORD = "delete"; + public static final String COMMAND_STRING = StudentCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Deletes the student identified by a STUDENT_IDENTIFIER (ID or full name)." + + "\nParameters: STUDENT_IDENTIFIER" + + "\nExample: " + COMMAND_STRING + " 1"; + + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted student: %1$s"; + + private final Identity identity; + + public DeleteStudentCommand(Identity identity) { + this.identity = identity; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Optional toDelete = model.getPersonByIdentity(identity); + if (toDelete.isEmpty()) { + throw new CommandException(Messages.MESSAGE_PERSON_NOT_FOUND); + } + + model.deletePerson(toDelete.get()); + model.updateFilteredPersonList(Model.FILTER_SHOW_ALL_PERSONS); + return new CommandResult.Builder(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(toDelete.get()))) + .withTab(Tab.student()) + .withReverseCommand(new AddStudentCommand(toDelete.get())) + .build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteStudentCommand otherDeleteCommand)) { + return false; + } + + return identity.equals(otherDeleteCommand.identity); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("identity", identity) + .toString(); + } +} diff --git a/src/main/java/tutorly/logic/commands/EditSessionCommand.java b/src/main/java/tutorly/logic/commands/EditSessionCommand.java new file mode 100644 index 00000000000..21459f1c16e --- /dev/null +++ b/src/main/java/tutorly/logic/commands/EditSessionCommand.java @@ -0,0 +1,184 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.commons.util.CollectionUtil.requireAllNonNull; +import static tutorly.logic.parser.CliSyntax.PREFIX_SUBJECT; +import static tutorly.logic.parser.CliSyntax.PREFIX_TIMESLOT; +import static tutorly.model.Model.FILTER_SHOW_ALL_SESSIONS; + +import java.util.Optional; + +import tutorly.commons.util.CollectionUtil; +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.session.Session; +import tutorly.model.session.Subject; +import tutorly.model.session.Timeslot; +import tutorly.ui.Tab; + +/** + * Edits the details of an existing session in the address book. + */ +public class EditSessionCommand extends SessionCommand { + public static final String COMMAND_WORD = "edit"; + public static final String COMMAND_STRING = SessionCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Edits the details of the session identified by a SESSION_ID. " + + "Existing values will be overwritten by the input values." + + "\nParameters: SESSION_ID " + + "[" + PREFIX_TIMESLOT + "TIMESLOT] " + + "[" + PREFIX_SUBJECT + "SUBJECT] " + + "\nExample: " + COMMAND_STRING + " 1 " + + PREFIX_TIMESLOT + "30 Mar 2025 11:30-13:30 " + + PREFIX_SUBJECT + "Mathematics"; + + public static final String MESSAGE_EDIT_SESSION_SUCCESS = "Edited session: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + + private final int sessionId; + private final EditSessionDescriptor editSessionDescriptor; + + /** + * @param id containing the ID or name of the session to edit + * @param editSessionDescriptor details to edit the session with + */ + public EditSessionCommand(int id, EditSessionDescriptor editSessionDescriptor) { + requireNonNull(editSessionDescriptor); + + this.sessionId = id; + this.editSessionDescriptor = new EditSessionDescriptor(editSessionDescriptor); + } + + /** + * Creates and returns a {@code Session} with the details of {@code sessionToEdit} + * edited with {@code editSessionDescriptor}. The ID of the session cannot be edited. + */ + private static Session createEditedSession(Session sessionToEdit, EditSessionDescriptor editSessionDescriptor) { + requireAllNonNull(sessionToEdit, editSessionDescriptor); + + Timeslot updatedTimeslot = editSessionDescriptor.getTimeslot().orElse(sessionToEdit.getTimeslot()); + Subject updatedSubject = editSessionDescriptor.getSubject().orElse(sessionToEdit.getSubject()); + + Session newSession = new Session(updatedTimeslot, updatedSubject); + newSession.setId(sessionToEdit.getId()); + + return newSession; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Optional sessionToEdit = model.getSessionById(sessionId); + if (sessionToEdit.isEmpty()) { + throw new CommandException(Messages.MESSAGE_SESSION_NOT_FOUND); + } + + Session editedSession = createEditedSession(sessionToEdit.get(), editSessionDescriptor); + + if (model.hasOverlappingSession(editedSession)) { + throw new CommandException(Messages.MESSAGE_SESSION_OVERLAP); + } + + model.setSession(sessionToEdit.get(), editedSession); + model.updateFilteredSessionList(FILTER_SHOW_ALL_SESSIONS); + + return new CommandResult.Builder(String.format(MESSAGE_EDIT_SESSION_SUCCESS, Messages.format(editedSession))) + .withTab(Tab.session(editedSession)) + .withReverseCommand(new EditSessionCommand( + sessionId, EditSessionDescriptor.fromSession(sessionToEdit.get()))) + .build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof EditSessionCommand e)) { + return false; + } + + return sessionId == e.sessionId + && editSessionDescriptor.equals(e.editSessionDescriptor); + } + + /** + * Stores the details to edit the session with. Each non-empty field value will replace the + * corresponding field value of the session. + */ + public static class EditSessionDescriptor { + private Timeslot timeslot; + private Subject subject; + + public EditSessionDescriptor() { + } + + /** + * Copy constructor. + */ + public EditSessionDescriptor(EditSessionDescriptor toCopy) { + setTimeslot(toCopy.timeslot); + setSubject(toCopy.subject); + } + + /** + * Returns a {@code EditSessionDescriptor} with the same values as {@code session}. + */ + public static EditSessionDescriptor fromSession(Session session) { + EditSessionDescriptor descriptor = new EditSessionDescriptor(); + descriptor.setTimeslot(session.getTimeslot()); + descriptor.setSubject(session.getSubject()); + return descriptor; + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(timeslot, subject); + } + + public Optional getTimeslot() { + return Optional.ofNullable(timeslot); + } + + public void setTimeslot(Timeslot date) { + this.timeslot = date; + } + + public Optional getSubject() { + return Optional.ofNullable(subject); + } + + public void setSubject(Subject subject) { + this.subject = subject; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof EditSessionDescriptor e)) { + return false; + } + + return getTimeslot().equals(e.getTimeslot()) + && getSubject().equals(e.getSubject()); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("timeslot", timeslot) + .add("subject", subject) + .toString(); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/tutorly/logic/commands/EditStudentCommand.java similarity index 56% rename from src/main/java/seedu/address/logic/commands/EditCommand.java rename to src/main/java/tutorly/logic/commands/EditStudentCommand.java index 4b581c7331e..6991e10eb0c 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/tutorly/logic/commands/EditStudentCommand.java @@ -1,96 +1,78 @@ -package seedu.address.logic.commands; +package tutorly.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static tutorly.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static tutorly.logic.parser.CliSyntax.PREFIX_EMAIL; +import static tutorly.logic.parser.CliSyntax.PREFIX_MEMO; +import static tutorly.logic.parser.CliSyntax.PREFIX_NAME; +import static tutorly.logic.parser.CliSyntax.PREFIX_PHONE; +import static tutorly.logic.parser.CliSyntax.PREFIX_TAG; +import static tutorly.model.Model.FILTER_SHOW_ALL_PERSONS; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; -import seedu.address.commons.util.ToStringBuilder; -import seedu.address.logic.Messages; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import tutorly.commons.util.CollectionUtil; +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.person.Address; +import tutorly.model.person.Email; +import tutorly.model.person.Identity; +import tutorly.model.person.Memo; +import tutorly.model.person.Name; +import tutorly.model.person.Person; +import tutorly.model.person.Phone; +import tutorly.model.tag.Tag; +import tutorly.ui.Tab; /** * Edits the details of an existing person in the address book. */ -public class EditCommand extends Command { +public class EditStudentCommand extends StudentCommand { public static final String COMMAND_WORD = "edit"; + public static final String COMMAND_STRING = StudentCommand.COMMAND_STRING + " " + COMMAND_WORD; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Edits the details of the student identified by a STUDENT_IDENTIFIER (ID or full name). " + + "Existing values will be overwritten by the input values." + + "\nParameters: STUDENT_IDENTIFIER " + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " 1 " + + "[" + PREFIX_TAG + "TAG]... " + + "[" + PREFIX_MEMO + "MEMO]" + + "\nExample: " + COMMAND_STRING + " 1 " + PREFIX_PHONE + "91234567 " + PREFIX_EMAIL + "johndoe@example.com"; - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; + public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited student: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - private final Index index; + private final Identity identity; private final EditPersonDescriptor editPersonDescriptor; /** - * @param index of the person in the filtered person list to edit + * @param identity containing the ID or name of the person to edit * @param editPersonDescriptor details to edit the person with */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { - requireNonNull(index); + public EditStudentCommand(Identity identity, EditPersonDescriptor editPersonDescriptor) { + requireNonNull(identity); requireNonNull(editPersonDescriptor); - this.index = index; + this.identity = identity; this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); } - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); - } - /** * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. + * edited with {@code editPersonDescriptor}. The ID of the person cannot be edited. */ private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { assert personToEdit != null; @@ -100,8 +82,35 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + Memo updatedMemo = editPersonDescriptor.getMemo().orElse(personToEdit.getMemo()); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + Person person = new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, updatedMemo); + person.setId(personToEdit.getId()); + return person; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Optional personToEdit = model.getPersonByIdentity(identity); + if (personToEdit.isEmpty()) { + throw new CommandException(Messages.MESSAGE_PERSON_NOT_FOUND); + } + + Person editedPerson = createEditedPerson(personToEdit.get(), editPersonDescriptor); + + if (!personToEdit.get().isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + throw new CommandException(Messages.MESSAGE_DUPLICATE_PERSON); + } + + model.setPerson(personToEdit.get(), editedPerson); + model.updateFilteredPersonList(FILTER_SHOW_ALL_PERSONS); + return new CommandResult.Builder(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))) + .withTab(Tab.student(editedPerson)) + .withReverseCommand(new EditStudentCommand( + new Identity(personToEdit.get().getId()), EditPersonDescriptor.fromPerson(personToEdit.get()))) + .build(); } @Override @@ -111,19 +120,18 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof EditCommand)) { + if (!(other instanceof EditStudentCommand otherEditCommand)) { return false; } - EditCommand otherEditCommand = (EditCommand) other; - return index.equals(otherEditCommand.index) + return identity.equals(otherEditCommand.identity) && editPersonDescriptor.equals(otherEditCommand.editPersonDescriptor); } @Override public String toString() { return new ToStringBuilder(this) - .add("index", index) + .add("identity", identity) .add("editPersonDescriptor", editPersonDescriptor) .toString(); } @@ -138,8 +146,10 @@ public static class EditPersonDescriptor { private Email email; private Address address; private Set tags; + private Memo memo; - public EditPersonDescriptor() {} + public EditPersonDescriptor() { + } /** * Copy constructor. @@ -151,45 +161,69 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setEmail(toCopy.email); setAddress(toCopy.address); setTags(toCopy.tags); + setMemo(toCopy.memo); + } + + /** + * Returns a {@code EditPersonDescriptor} with the same values as {@code person}. + */ + public static EditPersonDescriptor fromPerson(Person person) { + EditPersonDescriptor descriptor = new EditPersonDescriptor(); + descriptor.setName(person.getName()); + descriptor.setPhone(person.getPhone()); + descriptor.setEmail(person.getEmail()); + descriptor.setAddress(person.getAddress()); + descriptor.setTags(person.getTags()); + descriptor.setMemo(person.getMemo()); + return descriptor; } /** * 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, tags, memo); + } + + public Optional getName() { + return Optional.ofNullable(name); } public void setName(Name name) { this.name = name; } - public Optional getName() { - return Optional.ofNullable(name); + public Optional getPhone() { + return Optional.ofNullable(phone); } public void setPhone(Phone phone) { this.phone = phone; } - public Optional getPhone() { - return Optional.ofNullable(phone); + public Optional getEmail() { + return Optional.ofNullable(email); } public void setEmail(Email email) { this.email = email; } - public Optional getEmail() { - return Optional.ofNullable(email); + public Optional
getAddress() { + return Optional.ofNullable(address); } public void setAddress(Address address) { this.address = address; } - public Optional
getAddress() { - return Optional.ofNullable(address); + /** + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Optional> getTags() { + return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); } /** @@ -200,13 +234,12 @@ public void setTags(Set tags) { this.tags = (tags != null) ? new HashSet<>(tags) : null; } - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + public Optional getMemo() { + return Optional.ofNullable(memo); + } + + public void setMemo(Memo memo) { + this.memo = memo; } @Override @@ -216,16 +249,16 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { + if (!(other instanceof EditPersonDescriptor otherEditPersonDescriptor)) { return false; } - EditPersonDescriptor otherEditPersonDescriptor = (EditPersonDescriptor) other; return Objects.equals(name, otherEditPersonDescriptor.name) && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) && Objects.equals(address, otherEditPersonDescriptor.address) - && Objects.equals(tags, otherEditPersonDescriptor.tags); + && Objects.equals(tags, otherEditPersonDescriptor.tags) + && Objects.equals(memo, otherEditPersonDescriptor.memo); } @Override @@ -236,6 +269,7 @@ public String toString() { .add("email", email) .add("address", address) .add("tags", tags) + .add("memo", memo) .toString(); } } diff --git a/src/main/java/tutorly/logic/commands/EnrolSessionCommand.java b/src/main/java/tutorly/logic/commands/EnrolSessionCommand.java new file mode 100644 index 00000000000..6085f488509 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/EnrolSessionCommand.java @@ -0,0 +1,113 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; + +import java.util.Optional; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.attendancerecord.Feedback; +import tutorly.model.person.Identity; +import tutorly.model.person.Person; +import tutorly.model.session.Session; +import tutorly.ui.Tab; + +/** + * Creates a new AttendanceRecord for a student to a session. + */ +public class EnrolSessionCommand extends SessionCommand { + + public static final String COMMAND_WORD = "enrol"; + public static final String COMMAND_STRING = SessionCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Enrols a student identified by a STUDENT_IDENTIFIER (ID or full name) to a session." + + "\nParameters: STUDENT_IDENTIFIER " + + PREFIX_SESSION + "SESSION_ID" + + "\nExample: " + COMMAND_STRING + " 1 " + + PREFIX_SESSION + "2"; + + public static final String MESSAGE_SUCCESS = "%1$s enrolled to Session: %2$s"; + public static final String MESSAGE_DUPLICATE_ENROLMENT = "This student is already enrolled in the session"; + public static final boolean DEFAULT_PRESENCE = false; + + private final Identity identity; + private final int sessionId; + private final boolean presence; + private final Feedback feedback; + + /** + * Creates an EnrolSessionCommand for the given {@code Person} to the given {@code Session} with the given + * {@code presence} status and {@code feedback}. + */ + public EnrolSessionCommand(Identity identity, int sessionId, boolean presence, Feedback feedback) { + requireNonNull(identity); + requireNonNull(feedback); + this.identity = identity; + this.sessionId = sessionId; + this.presence = presence; + this.feedback = feedback; + } + + /** + * Creates an EnrolSessionCommand for the given {@code Person} to the given {@code Session}. + */ + public EnrolSessionCommand(Identity identity, int sessionId) { + this(identity, sessionId, DEFAULT_PRESENCE, Feedback.empty()); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Optional person = model.getPersonByIdentity(identity); + if (person.isEmpty()) { + throw new CommandException(Messages.MESSAGE_PERSON_NOT_FOUND); + } + + Optional session = model.getSessionById(sessionId); + if (session.isEmpty()) { + throw new CommandException(Messages.MESSAGE_SESSION_NOT_FOUND); + } + + AttendanceRecord record = new AttendanceRecord(person.get().getId(), sessionId, presence, feedback); + if (model.hasAttendanceRecord(record)) { + throw new CommandException(MESSAGE_DUPLICATE_ENROLMENT); + } + + model.addAttendanceRecord(record); + model.updateFilteredSessionList(Model.FILTER_SHOW_ALL_SESSIONS); + return new CommandResult.Builder( + String.format(MESSAGE_SUCCESS, person.get().getName().fullName, Messages.format(session.get()))) + .withTab(Tab.attendanceRecord(session.get(), record)) + .withReverseCommand(new UnenrolSessionCommand(identity, sessionId)) + .build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EnrolSessionCommand otherEnrolSessionCommand)) { + return false; + } + + return identity.equals(otherEnrolSessionCommand.identity) + && sessionId == otherEnrolSessionCommand.sessionId; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("identity", identity) + .add("sessionId", sessionId) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/tutorly/logic/commands/ExitCommand.java similarity index 53% rename from src/main/java/seedu/address/logic/commands/ExitCommand.java rename to src/main/java/tutorly/logic/commands/ExitCommand.java index 3dd85a8ba90..9d349ec8257 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/tutorly/logic/commands/ExitCommand.java @@ -1,6 +1,6 @@ -package seedu.address.logic.commands; +package tutorly.logic.commands; -import seedu.address.model.Model; +import tutorly.model.Model; /** * Terminates the program. @@ -8,12 +8,13 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; + public static final String COMMAND_STRING = COMMAND_WORD; - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting app as requested ..."; @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult.Builder(MESSAGE_EXIT_ACKNOWLEDGEMENT).exit().build(); } } diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/tutorly/logic/commands/HelpCommand.java similarity index 57% rename from src/main/java/seedu/address/logic/commands/HelpCommand.java rename to src/main/java/tutorly/logic/commands/HelpCommand.java index bf824f91bd0..0b5f270d62e 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/tutorly/logic/commands/HelpCommand.java @@ -1,6 +1,6 @@ -package seedu.address.logic.commands; +package tutorly.logic.commands; -import seedu.address.model.Model; +import tutorly.model.Model; /** * Format full help instructions for every command for display. @@ -8,14 +8,15 @@ public class HelpCommand extends Command { public static final String COMMAND_WORD = "help"; + public static final String COMMAND_STRING = COMMAND_WORD; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" - + "Example: " + COMMAND_WORD; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions." + + "\n\nExample: " + COMMAND_WORD; public static final String SHOWING_HELP_MESSAGE = "Opened help window."; @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult.Builder(SHOWING_HELP_MESSAGE).showHelp().build(); } } diff --git a/src/main/java/tutorly/logic/commands/ListSessionCommand.java b/src/main/java/tutorly/logic/commands/ListSessionCommand.java new file mode 100644 index 00000000000..2b956d9051d --- /dev/null +++ b/src/main/java/tutorly/logic/commands/ListSessionCommand.java @@ -0,0 +1,26 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.model.Model.FILTER_SHOW_ALL_SESSIONS; + +import tutorly.model.Model; +import tutorly.ui.Tab; + +/** + * Lists all sessions in the address book to the user. + */ +public class ListSessionCommand extends SessionCommand { + + public static final String COMMAND_WORD = "list"; + public static final String COMMAND_STRING = SessionCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Listed all sessions"; + + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredSessionList(FILTER_SHOW_ALL_SESSIONS); + return new CommandResult.Builder(MESSAGE_SUCCESS).withTab(Tab.session()).build(); + } +} diff --git a/src/main/java/tutorly/logic/commands/ListStudentCommand.java b/src/main/java/tutorly/logic/commands/ListStudentCommand.java new file mode 100644 index 00000000000..ab2b09128b2 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/ListStudentCommand.java @@ -0,0 +1,26 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.model.Model.FILTER_SHOW_ALL_PERSONS; + +import tutorly.model.Model; +import tutorly.ui.Tab; + +/** + * Lists all persons in the address book to the user. + */ +public class ListStudentCommand extends StudentCommand { + + public static final String COMMAND_WORD = "list"; + public static final String COMMAND_STRING = StudentCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Listed all persons"; + + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(FILTER_SHOW_ALL_PERSONS); + return new CommandResult.Builder(MESSAGE_SUCCESS).withTab(Tab.student()).build(); + } +} diff --git a/src/main/java/tutorly/logic/commands/SearchSessionCommand.java b/src/main/java/tutorly/logic/commands/SearchSessionCommand.java new file mode 100644 index 00000000000..4382b63246a --- /dev/null +++ b/src/main/java/tutorly/logic/commands/SearchSessionCommand.java @@ -0,0 +1,67 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.logic.parser.CliSyntax.PREFIX_DATE; +import static tutorly.logic.parser.CliSyntax.PREFIX_SUBJECT; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.model.Model; +import tutorly.model.filter.Filter; +import tutorly.model.session.Session; +import tutorly.ui.Tab; + +/** + * Finds and lists all sessions on a particular date or whose subject contains any of the keywords. + * Keyword matching is case-insensitive. + */ +public class SearchSessionCommand extends SessionCommand { + public static final String COMMAND_WORD = "search"; + + public static final String COMMAND_STRING = SessionCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Searches for all sessions on a particular date, or " + + "whose subject contain any of the specified keywords (case-insensitive) and displays them as a list." + + "\nParameters: " + + "[" + PREFIX_DATE + "DATE] " + + "[" + PREFIX_SUBJECT + "SUBJECT_KEYWORDS]" + + "\nExample: " + COMMAND_STRING + " " + PREFIX_DATE + "18 Mar 2025 " + PREFIX_SUBJECT + "Math Eng"; + + private final Filter filter; + + public SearchSessionCommand(Filter filter) { + this.filter = filter; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredSessionList(filter); + return new CommandResult.Builder( + String.format(Messages.MESSAGE_SESSIONS_LISTED_OVERVIEW, model.getFilteredSessionList().size())) + .withTab(Tab.session()) + .build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SearchSessionCommand otherSearchCommand)) { + return false; + } + + return filter.equals(otherSearchCommand.filter); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("filter", filter) + .toString(); + } +} diff --git a/src/main/java/tutorly/logic/commands/SearchStudentCommand.java b/src/main/java/tutorly/logic/commands/SearchStudentCommand.java new file mode 100644 index 00000000000..ed71045bd91 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/SearchStudentCommand.java @@ -0,0 +1,71 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.logic.parser.CliSyntax.PREFIX_NAME; +import static tutorly.logic.parser.CliSyntax.PREFIX_PHONE; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.model.Model; +import tutorly.model.filter.Filter; +import tutorly.model.person.Person; +import tutorly.ui.Tab; + +/** + * Finds and lists all persons in address book whose fields contains any of the argument keywords, or attends the + * session with the session id. + * Keyword matching is case-insensitive. + */ +public class SearchStudentCommand extends StudentCommand { + + public static final String COMMAND_WORD = "search"; + public static final String COMMAND_STRING = StudentCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Searches for all students who attended a session or " + + "whose fields contain any of the specified keywords (case-insensitive) and displays them as a list." + + "\nParameters: " + + "[" + PREFIX_SESSION + "SESSION_ID] " + + "[" + PREFIX_NAME + "NAME_KEYWORDS] " + + "[" + PREFIX_PHONE + "PHONE_KEYWORDS]" + + "\nExample: " + COMMAND_STRING + " " + PREFIX_SESSION + "1 " + PREFIX_NAME + "ali bob charli " + + PREFIX_PHONE + "9124 86192"; + + private final Filter filter; + + public SearchStudentCommand(Filter filter) { + this.filter = filter; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(filter); + return new CommandResult.Builder( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())) + .withTab(Tab.student()) + .build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SearchStudentCommand otherSearchCommand)) { + return false; + } + + return filter.equals(otherSearchCommand.filter); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("filter", filter) + .toString(); + } +} diff --git a/src/main/java/tutorly/logic/commands/SessionCommand.java b/src/main/java/tutorly/logic/commands/SessionCommand.java new file mode 100644 index 00000000000..cac96e93148 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/SessionCommand.java @@ -0,0 +1,21 @@ +package tutorly.logic.commands; + +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.ui.Tab; + +/** + * Represents a command related to sessions. + */ +public class SessionCommand extends Command { + + public static final String COMMAND_WORD = "session"; + public static final String COMMAND_STRING = COMMAND_WORD; + + @Override + public CommandResult execute(Model model) throws CommandException { + return new CommandResult.Builder(Messages.MESSAGE_SESSIONS_SHOWN).withTab(Tab.session()).build(); + } + +} diff --git a/src/main/java/tutorly/logic/commands/StudentCommand.java b/src/main/java/tutorly/logic/commands/StudentCommand.java new file mode 100644 index 00000000000..5b5896bb81c --- /dev/null +++ b/src/main/java/tutorly/logic/commands/StudentCommand.java @@ -0,0 +1,21 @@ +package tutorly.logic.commands; + +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.ui.Tab; + +/** + * Represents a command related to students. + */ +public class StudentCommand extends Command { + + public static final String COMMAND_WORD = "student"; + public static final String COMMAND_STRING = COMMAND_WORD; + + @Override + public CommandResult execute(Model model) throws CommandException { + return new CommandResult.Builder(Messages.MESSAGE_PERSONS_SHOWN).withTab(Tab.student()).build(); + } + +} diff --git a/src/main/java/tutorly/logic/commands/UndoCommand.java b/src/main/java/tutorly/logic/commands/UndoCommand.java new file mode 100644 index 00000000000..3cf867df387 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/UndoCommand.java @@ -0,0 +1,20 @@ +package tutorly.logic.commands; + +import tutorly.model.Model; + +/** + * Undoes the last command executed. + */ +public class UndoCommand extends Command { + + public static final String COMMAND_WORD = "undo"; + public static final String COMMAND_STRING = COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Last command undone!"; + + @Override + public CommandResult execute(Model model) { + return new CommandResult.Builder(MESSAGE_SUCCESS).reverseLast().build(); + } + +} diff --git a/src/main/java/tutorly/logic/commands/UnenrolSessionCommand.java b/src/main/java/tutorly/logic/commands/UnenrolSessionCommand.java new file mode 100644 index 00000000000..c90c1f34fb0 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/UnenrolSessionCommand.java @@ -0,0 +1,102 @@ +package tutorly.logic.commands; + +import static java.util.Objects.requireNonNull; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; + +import java.util.Optional; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.attendancerecord.Feedback; +import tutorly.model.person.Identity; +import tutorly.model.person.Person; +import tutorly.model.session.Session; +import tutorly.ui.Tab; + +/** + * Removes an AttendanceRecord for a student to a session. + */ +public class UnenrolSessionCommand extends SessionCommand { + public static final String COMMAND_WORD = "unenrol"; + public static final String COMMAND_STRING = SessionCommand.COMMAND_STRING + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Unenrols a student identified by a STUDENT_IDENTIFIER (ID or full name) from a session." + + "\nParameters: STUDENT_IDENTIFIER " + + PREFIX_SESSION + "SESSION_ID" + + "\nExample: " + COMMAND_STRING + " 1 " + + PREFIX_SESSION + "2"; + + public static final String MESSAGE_SUCCESS = "%1$s has been unenrolled from Session: %2$s"; + public static final String MESSAGE_MISSING_ENROLMENT = "%1$s is not enrolled to Session: %2$s"; + + private final Identity identity; + private final int sessionId; + + /** + * Creates an UnenrolSessionCommand for the given {@code Person} to the given {@code Session} + */ + public UnenrolSessionCommand(Identity identity, int sessionId) { + requireNonNull(identity); + this.identity = identity; + this.sessionId = sessionId; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Optional person = model.getPersonByIdentity(identity); + if (person.isEmpty()) { + throw new CommandException(Messages.MESSAGE_PERSON_NOT_FOUND); + } + + Optional session = model.getSessionById(sessionId); + if (session.isEmpty()) { + throw new CommandException(Messages.MESSAGE_SESSION_NOT_FOUND); + } + + // value of isPresent is not used when checking if a record is in AddressBook, set to false as a placeholder + Optional record = model.findAttendanceRecord( + new AttendanceRecord(person.get().getId(), sessionId, false, Feedback.empty())); + if (record.isEmpty()) { + throw new CommandException(String.format( + MESSAGE_MISSING_ENROLMENT, person.get().getName().fullName, Messages.format(session.get()))); + } + + model.removeAttendanceRecord(record.get()); + model.updateFilteredSessionList(Model.FILTER_SHOW_ALL_SESSIONS); + return new CommandResult.Builder( + String.format(MESSAGE_SUCCESS, person.get().getName().fullName, Messages.format(session.get()))) + .withTab(Tab.session(session.get())) + .withReverseCommand(new EnrolSessionCommand( + identity, sessionId, record.get().getAttendance(), record.get().getFeedback())) + .build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof UnenrolSessionCommand otherUnenrolSessionCommand)) { + return false; + } + + return identity.equals(otherUnenrolSessionCommand.identity) + && sessionId == otherUnenrolSessionCommand.sessionId; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("identity", identity) + .add("sessionId", sessionId) + .toString(); + } +} diff --git a/src/main/java/tutorly/logic/commands/ViewSessionCommand.java b/src/main/java/tutorly/logic/commands/ViewSessionCommand.java new file mode 100644 index 00000000000..1527dca6c35 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/ViewSessionCommand.java @@ -0,0 +1,38 @@ +package tutorly.logic.commands; + +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.session.Session; +import tutorly.ui.Tab; + +/** + * Shows a session. + */ +public class ViewSessionCommand extends SessionCommand { + + public static final String COMMAND_WORD = "view"; + public static final String COMMAND_STRING = SessionCommand.COMMAND_STRING + " " + COMMAND_WORD; + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Shows the session identified by a SESSION_ID." + + "\nParameters: SESSION_ID" + + "\nExample: " + COMMAND_STRING + " 1"; + + private final int sessionId; + + public ViewSessionCommand(int sessionId) { + this.sessionId = sessionId; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + Session session = model.getSessionById(sessionId) + .orElseThrow(() -> new CommandException(Messages.MESSAGE_SESSION_NOT_FOUND)); + + model.updateFilteredSessionList(Model.FILTER_SHOW_ALL_SESSIONS); + return new CommandResult.Builder(String.format(Messages.MESSAGE_SESSION_SHOWN, Messages.format(session))) + .withTab(Tab.session(session)) + .build(); + } + +} diff --git a/src/main/java/tutorly/logic/commands/ViewStudentCommand.java b/src/main/java/tutorly/logic/commands/ViewStudentCommand.java new file mode 100644 index 00000000000..e98e5610000 --- /dev/null +++ b/src/main/java/tutorly/logic/commands/ViewStudentCommand.java @@ -0,0 +1,39 @@ +package tutorly.logic.commands; + +import tutorly.logic.Messages; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.model.Model; +import tutorly.model.person.Identity; +import tutorly.model.person.Person; +import tutorly.ui.Tab; + +/** + * Shows a student. + */ +public class ViewStudentCommand extends StudentCommand { + + public static final String COMMAND_WORD = "view"; + public static final String COMMAND_STRING = StudentCommand.COMMAND_STRING + " " + COMMAND_WORD; + public static final String MESSAGE_USAGE = COMMAND_STRING + + ": Shows the student identified by a STUDENT_IDENTIFIER." + + "\nParameters: STUDENT_IDENTIFIER" + + "\nExample: " + COMMAND_STRING + " 1"; + + private final Identity identity; + + public ViewStudentCommand(Identity identity) { + this.identity = identity; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + Person student = model.getPersonByIdentity(identity) + .orElseThrow(() -> new CommandException(Messages.MESSAGE_PERSON_NOT_FOUND)); + + model.updateFilteredPersonList(Model.FILTER_SHOW_ALL_PERSONS); + return new CommandResult.Builder(String.format(Messages.MESSAGE_PERSON_SHOWN, Messages.format(student))) + .withTab(Tab.student(student)) + .build(); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/tutorly/logic/commands/exceptions/CommandException.java similarity index 89% rename from src/main/java/seedu/address/logic/commands/exceptions/CommandException.java rename to src/main/java/tutorly/logic/commands/exceptions/CommandException.java index a16bd14f2cd..2b5c7afe465 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/tutorly/logic/commands/exceptions/CommandException.java @@ -1,4 +1,4 @@ -package seedu.address.logic.commands.exceptions; +package tutorly.logic.commands.exceptions; /** * Represents an error which occurs during execution of a {@link Command}. diff --git a/src/main/java/tutorly/logic/parser/AddSessionCommandParser.java b/src/main/java/tutorly/logic/parser/AddSessionCommandParser.java new file mode 100644 index 00000000000..7a25abc49ac --- /dev/null +++ b/src/main/java/tutorly/logic/parser/AddSessionCommandParser.java @@ -0,0 +1,48 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.CliSyntax.PREFIX_SUBJECT; +import static tutorly.logic.parser.CliSyntax.PREFIX_TIMESLOT; + +import java.util.stream.Stream; + +import tutorly.logic.commands.AddSessionCommand; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.session.Session; +import tutorly.model.session.Subject; +import tutorly.model.session.Timeslot; + +/** + * Parses input arguments and creates a new AddSessionCommand object. + */ +public class AddSessionCommandParser implements Parser { + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + /** + * Parses the given {@code String} of arguments in the context of the AddSessionCommand + * and returns an AddSessionCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format. + */ + public AddSessionCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TIMESLOT, PREFIX_SUBJECT); + + if (!arePrefixesPresent(argMultimap, PREFIX_TIMESLOT, PREFIX_SUBJECT) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddSessionCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_TIMESLOT, PREFIX_SUBJECT); + Timeslot timeslot = ParserUtil.parseTimeslot(argMultimap.getValue(PREFIX_TIMESLOT).get()); + Subject subject = ParserUtil.parseSubject(argMultimap.getValue(PREFIX_SUBJECT).get()); + + Session session = new Session(timeslot, subject); + return new AddSessionCommand(session); + } +} diff --git a/src/main/java/tutorly/logic/parser/AddStudentCommandParser.java b/src/main/java/tutorly/logic/parser/AddStudentCommandParser.java new file mode 100644 index 00000000000..562b7322369 --- /dev/null +++ b/src/main/java/tutorly/logic/parser/AddStudentCommandParser.java @@ -0,0 +1,88 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static tutorly.logic.parser.CliSyntax.PREFIX_EMAIL; +import static tutorly.logic.parser.CliSyntax.PREFIX_MEMO; +import static tutorly.logic.parser.CliSyntax.PREFIX_NAME; +import static tutorly.logic.parser.CliSyntax.PREFIX_PHONE; +import static tutorly.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Set; +import java.util.stream.Stream; + +import tutorly.logic.commands.AddStudentCommand; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.person.Address; +import tutorly.model.person.Email; +import tutorly.model.person.Memo; +import tutorly.model.person.Name; +import tutorly.model.person.Person; +import tutorly.model.person.Phone; +import tutorly.model.tag.Tag; + +/** + * Parses input arguments and creates a new AddCommand object + */ +public class AddStudentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddStudentCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize( + args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG, PREFIX_MEMO); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddStudentCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_MEMO); + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + + Phone phone; + if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { + phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); + } else { + phone = Phone.empty(); + } + + Email email; + if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { + email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); + } else { + email = Email.empty(); + } + + Address address; + if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { + address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + } else { + address = Address.empty(); + } + + Memo memo; + if (argMultimap.getValue(PREFIX_MEMO).isPresent()) { + memo = ParserUtil.parseMemo(argMultimap.getValue(PREFIX_MEMO).get()); + } else { + memo = Memo.empty(); + } + + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + + Person person = new Person(name, phone, email, address, tagList, memo); + + return new AddStudentCommand(person); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/tutorly/logic/parser/AddressBookParser.java similarity index 52% rename from src/main/java/seedu/address/logic/parser/AddressBookParser.java rename to src/main/java/tutorly/logic/parser/AddressBookParser.java index 3149ee07e0b..8bfbacb55c3 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/tutorly/logic/parser/AddressBookParser.java @@ -1,28 +1,26 @@ -package seedu.address.logic.parser; +package tutorly.logic.parser; -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.Messages.MESSAGE_UNKNOWN_COMMAND; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.parser.exceptions.ParseException; +import tutorly.commons.core.LogsCenter; +import tutorly.logic.commands.ClearCommand; +import tutorly.logic.commands.Command; +import tutorly.logic.commands.ExitCommand; +import tutorly.logic.commands.HelpCommand; +import tutorly.logic.commands.SessionCommand; +import tutorly.logic.commands.StudentCommand; +import tutorly.logic.commands.UndoCommand; +import tutorly.logic.parser.exceptions.ParseException; /** * Parses user input. */ -public class AddressBookParser { +public class AddressBookParser implements Parser { /** * Used for initial separation of command word and args. @@ -37,7 +35,11 @@ public class AddressBookParser { * @return the command based on the user input * @throws ParseException if the user input does not conform the expected format */ - public Command parseCommand(String userInput) throws ParseException { + public Command parse(String userInput) throws ParseException { + if (userInput.isBlank()) { + return defaultCommand(); + } + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); if (!matcher.matches()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); @@ -51,36 +53,46 @@ public Command parseCommand(String userInput) throws ParseException { // Lower level log messages are used sparingly to minimize noise in the code. logger.fine("Command word: " + commandWord + "; Arguments: " + arguments); - switch (commandWord) { + try { + return parseCommand(commandWord, arguments); + } catch (ParseException e) { + logger.finer("This user input caused a ParseException: " + userInput); + throw e; + } + } - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); + /** + * Parses a command word and arguments into a command. + */ + protected Command parseCommand(String command, String args) throws ParseException { + command = command.toLowerCase(); - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); + switch (command) { + case StudentCommand.COMMAND_WORD: + return new StudentCommandParser().parse(args); - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); + case SessionCommand.COMMAND_WORD: + return new SessionCommandParser().parse(args); case ClearCommand.COMMAND_WORD: return new ClearCommand(); - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - case ExitCommand.COMMAND_WORD: return new ExitCommand(); + case UndoCommand.COMMAND_WORD: + return new UndoCommand(); + case HelpCommand.COMMAND_WORD: return new HelpCommand(); default: - logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } } + protected Command defaultCommand() throws ParseException { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/tutorly/logic/parser/ArgumentMultimap.java similarity index 95% rename from src/main/java/seedu/address/logic/parser/ArgumentMultimap.java rename to src/main/java/tutorly/logic/parser/ArgumentMultimap.java index 21e26887a83..c63da8ec6be 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/tutorly/logic/parser/ArgumentMultimap.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package tutorly.logic.parser; import java.util.ArrayList; import java.util.HashMap; @@ -7,8 +7,8 @@ import java.util.Optional; import java.util.stream.Stream; -import seedu.address.logic.Messages; -import seedu.address.logic.parser.exceptions.ParseException; +import tutorly.logic.Messages; +import tutorly.logic.parser.exceptions.ParseException; /** * Stores mapping of prefixes to their respective arguments. diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/tutorly/logic/parser/ArgumentTokenizer.java similarity index 99% rename from src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java rename to src/main/java/tutorly/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..3ddd91f2263 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/tutorly/logic/parser/ArgumentTokenizer.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package tutorly.logic.parser; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/tutorly/logic/parser/AttendanceFeedbackCommandParser.java b/src/main/java/tutorly/logic/parser/AttendanceFeedbackCommandParser.java new file mode 100644 index 00000000000..cc82745554c --- /dev/null +++ b/src/main/java/tutorly/logic/parser/AttendanceFeedbackCommandParser.java @@ -0,0 +1,48 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.CliSyntax.PREFIX_FEEDBACK; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; +import static tutorly.logic.parser.ParserUtil.parseFeedback; +import static tutorly.logic.parser.ParserUtil.parseSessionId; + +import java.util.Optional; + +import tutorly.logic.commands.AttendanceFeedbackCommand; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.attendancerecord.Feedback; +import tutorly.model.person.Identity; + +/** + * Parses input arguments and creates a new AttendanceFeedbackCommandParser object + */ +public class AttendanceFeedbackCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the AttendanceFeedbackCommand + * and returns a AttendanceFeedbackCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public AttendanceFeedbackCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SESSION, PREFIX_FEEDBACK); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_SESSION, PREFIX_FEEDBACK); + + Feedback feedback; + if (argMultimap.getValue(PREFIX_FEEDBACK).isPresent()) { + feedback = parseFeedback(argMultimap.getValue(PREFIX_FEEDBACK).get()); + } else { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AttendanceFeedbackCommand.MESSAGE_USAGE)); + } + + Optional sessionId = argMultimap.getValue(PREFIX_SESSION); + if (sessionId.isEmpty() || sessionId.get().isBlank() || argMultimap.getPreamble().isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AttendanceFeedbackCommand.MESSAGE_USAGE)); + } + + Identity identity = ParserUtil.parseIdentity(argMultimap.getPreamble()); + + return new AttendanceFeedbackCommand(identity, parseSessionId(sessionId.get()), feedback); + } +} diff --git a/src/main/java/tutorly/logic/parser/AttendanceMarkSessionCommandParser.java b/src/main/java/tutorly/logic/parser/AttendanceMarkSessionCommandParser.java new file mode 100644 index 00000000000..e30f18702d1 --- /dev/null +++ b/src/main/java/tutorly/logic/parser/AttendanceMarkSessionCommandParser.java @@ -0,0 +1,36 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; +import static tutorly.logic.parser.ParserUtil.parseSessionId; + +import java.util.Optional; + +import tutorly.logic.commands.AttendanceMarkSessionCommand; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.person.Identity; + +/** + * Parses input arguments and creates a new AttendanceMarkSessionCommandParser object + */ +public class AttendanceMarkSessionCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the AttendanceMarkSessionCommand + * and returns a AttendanceMarkSessionCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public AttendanceMarkSessionCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SESSION); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_SESSION); + Optional sessionId = argMultimap.getValue(PREFIX_SESSION); + + if (sessionId.isEmpty() || sessionId.get().isBlank() || argMultimap.getPreamble().isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AttendanceMarkSessionCommand.MESSAGE_USAGE)); + } + + Identity identity = ParserUtil.parseIdentity(argMultimap.getPreamble()); + return new AttendanceMarkSessionCommand(identity, parseSessionId(sessionId.get())); + } +} diff --git a/src/main/java/tutorly/logic/parser/AttendanceUnmarkSessionCommandParser.java b/src/main/java/tutorly/logic/parser/AttendanceUnmarkSessionCommandParser.java new file mode 100644 index 00000000000..ce16129e410 --- /dev/null +++ b/src/main/java/tutorly/logic/parser/AttendanceUnmarkSessionCommandParser.java @@ -0,0 +1,36 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; +import static tutorly.logic.parser.ParserUtil.parseSessionId; + +import java.util.Optional; + +import tutorly.logic.commands.AttendanceUnmarkSessionCommand; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.person.Identity; + +/** + * Parses input arguments and creates a new AttendanceUnmarkSessionCommandParser object + */ +public class AttendanceUnmarkSessionCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the AttendanceUnmarkSessionCommand + * and returns a AttendanceUnmarkSessionCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public AttendanceUnmarkSessionCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SESSION); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_SESSION); + Optional sessionId = argMultimap.getValue(PREFIX_SESSION); + + if (sessionId.isEmpty() || sessionId.get().isBlank() || argMultimap.getPreamble().isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AttendanceUnmarkSessionCommand.MESSAGE_USAGE)); + } + + Identity identity = ParserUtil.parseIdentity(argMultimap.getPreamble()); + return new AttendanceUnmarkSessionCommand(identity, parseSessionId(sessionId.get())); + } +} diff --git a/src/main/java/tutorly/logic/parser/CliSyntax.java b/src/main/java/tutorly/logic/parser/CliSyntax.java new file mode 100644 index 00000000000..4c0eeb7adda --- /dev/null +++ b/src/main/java/tutorly/logic/parser/CliSyntax.java @@ -0,0 +1,22 @@ +package tutorly.logic.parser; + +/** + * Contains Command Line Interface (CLI) syntax definitions common to multiple commands + */ +public class CliSyntax { + + /* Prefix definitions for students */ + public static final Prefix PREFIX_NAME = new Prefix("n/"); + public static final Prefix PREFIX_PHONE = new Prefix("p/"); + public static final Prefix PREFIX_EMAIL = new Prefix("e/"); + public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_MEMO = new Prefix("m/"); + + /* Prefix definitions for sessions */ + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_TIMESLOT = new Prefix("t/"); + public static final Prefix PREFIX_SUBJECT = new Prefix("sub/"); + public static final Prefix PREFIX_SESSION = new Prefix("ses/"); + public static final Prefix PREFIX_FEEDBACK = new Prefix("f/"); +} diff --git a/src/main/java/tutorly/logic/parser/DeleteSessionCommandParser.java b/src/main/java/tutorly/logic/parser/DeleteSessionCommandParser.java new file mode 100644 index 00000000000..10a81e7881f --- /dev/null +++ b/src/main/java/tutorly/logic/parser/DeleteSessionCommandParser.java @@ -0,0 +1,29 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.ParserUtil.parseSessionId; + +import tutorly.logic.commands.DeleteSessionCommand; +import tutorly.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteSessionCommand object + */ +public class DeleteSessionCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteSessionCommand + * and returns a DeleteSessionCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + public DeleteSessionCommand parse(String args) throws ParseException { + try { + int sessionId = parseSessionId(args.trim()); + return new DeleteSessionCommand(sessionId); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteSessionCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/tutorly/logic/parser/DeleteStudentCommandParser.java b/src/main/java/tutorly/logic/parser/DeleteStudentCommandParser.java new file mode 100644 index 00000000000..d64d324d507 --- /dev/null +++ b/src/main/java/tutorly/logic/parser/DeleteStudentCommandParser.java @@ -0,0 +1,30 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import tutorly.logic.commands.DeleteStudentCommand; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.person.Identity; + +/** + * Parses input arguments and creates a new DeleteCommand object + */ +public class DeleteStudentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteCommand + * and returns a DeleteCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteStudentCommand parse(String args) throws ParseException { + try { + Identity identity = ParserUtil.parseIdentity(args); + return new DeleteStudentCommand(identity); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteStudentCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/tutorly/logic/parser/EditSessionCommandParser.java b/src/main/java/tutorly/logic/parser/EditSessionCommandParser.java new file mode 100644 index 00000000000..ce6186d083f --- /dev/null +++ b/src/main/java/tutorly/logic/parser/EditSessionCommandParser.java @@ -0,0 +1,56 @@ +package tutorly.logic.parser; + +import static java.util.Objects.requireNonNull; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; +import static tutorly.logic.parser.CliSyntax.PREFIX_SUBJECT; +import static tutorly.logic.parser.CliSyntax.PREFIX_TIMESLOT; + +import tutorly.logic.Messages; +import tutorly.logic.commands.EditSessionCommand; +import tutorly.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditSessionCommand object + */ +public class EditSessionCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditSessionCommand + * and returns an EditSessionCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public EditSessionCommand parse(String args) throws ParseException { + requireNonNull(args); + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, + PREFIX_SESSION, + PREFIX_TIMESLOT, + PREFIX_SUBJECT); + + int sessionId; + try { + sessionId = ParserUtil.parseSessionId(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException( + String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, EditSessionCommand.MESSAGE_USAGE), pe); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_SESSION, PREFIX_TIMESLOT, PREFIX_SUBJECT); + + EditSessionCommand.EditSessionDescriptor editSessionDescriptor = new EditSessionCommand.EditSessionDescriptor(); + + if (argMultimap.getValue(PREFIX_TIMESLOT).isPresent()) { + editSessionDescriptor.setTimeslot(ParserUtil.parseTimeslot(argMultimap.getValue(PREFIX_TIMESLOT).get())); + } + if (argMultimap.getValue(PREFIX_SUBJECT).isPresent()) { + editSessionDescriptor.setSubject(ParserUtil.parseSubject(argMultimap.getValue(PREFIX_SUBJECT).get())); + } + + if (!editSessionDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditSessionCommand.MESSAGE_NOT_EDITED); + } + + return new EditSessionCommand(sessionId, editSessionDescriptor); + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/tutorly/logic/parser/EditStudentCommandParser.java similarity index 57% rename from src/main/java/seedu/address/logic/parser/EditCommandParser.java rename to src/main/java/tutorly/logic/parser/EditStudentCommandParser.java index 46b3309a78b..efd128e7bf0 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/tutorly/logic/parser/EditStudentCommandParser.java @@ -1,48 +1,51 @@ -package seedu.address.logic.parser; +package tutorly.logic.parser; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static tutorly.logic.parser.CliSyntax.PREFIX_EMAIL; +import static tutorly.logic.parser.CliSyntax.PREFIX_MEMO; +import static tutorly.logic.parser.CliSyntax.PREFIX_NAME; +import static tutorly.logic.parser.CliSyntax.PREFIX_PHONE; +import static tutorly.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Collection; import java.util.Collections; import java.util.Optional; import java.util.Set; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; +import tutorly.logic.commands.EditStudentCommand; +import tutorly.logic.commands.EditStudentCommand.EditPersonDescriptor; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.person.Identity; +import tutorly.model.tag.Tag; /** * Parses input arguments and creates a new EditCommand object */ -public class EditCommandParser implements Parser { +public class EditStudentCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the EditCommand * and returns an EditCommand object for execution. + * * @throws ParseException if the user input does not conform the expected format */ - public EditCommand parse(String args) throws ParseException { + public EditStudentCommand parse(String args) throws ParseException { requireNonNull(args); - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize( + args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG, PREFIX_MEMO); - Index index; + Identity identity; try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); + identity = ParserUtil.parseIdentity(argMultimap.getPreamble()); } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditStudentCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_MEMO); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -58,13 +61,16 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } + if (argMultimap.getValue(PREFIX_MEMO).isPresent()) { + editPersonDescriptor.setMemo(ParserUtil.parseMemo(argMultimap.getValue(PREFIX_MEMO).get())); + } parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + throw new ParseException(EditStudentCommand.MESSAGE_NOT_EDITED); } - return new EditCommand(index, editPersonDescriptor); + return new EditStudentCommand(identity, editPersonDescriptor); } /** diff --git a/src/main/java/tutorly/logic/parser/EnrolSessionCommandParser.java b/src/main/java/tutorly/logic/parser/EnrolSessionCommandParser.java new file mode 100644 index 00000000000..b0d19d6135b --- /dev/null +++ b/src/main/java/tutorly/logic/parser/EnrolSessionCommandParser.java @@ -0,0 +1,36 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; +import static tutorly.logic.parser.ParserUtil.parseSessionId; + +import java.util.Optional; + +import tutorly.logic.commands.EnrolSessionCommand; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.person.Identity; + +/** + * Parses input arguments and creates a new EnrolSessionCommand object + */ +public class EnrolSessionCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the EnrolSessionCommand + * and returns a EnrolSessionCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public EnrolSessionCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SESSION); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_SESSION); + Optional sessionId = argMultimap.getValue(PREFIX_SESSION); + + if (sessionId.isEmpty() || sessionId.get().isBlank() || argMultimap.getPreamble().isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EnrolSessionCommand.MESSAGE_USAGE)); + } + + Identity identity = ParserUtil.parseIdentity(argMultimap.getPreamble()); + return new EnrolSessionCommand(identity, parseSessionId(sessionId.get())); + } +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/tutorly/logic/parser/Parser.java similarity index 72% rename from src/main/java/seedu/address/logic/parser/Parser.java rename to src/main/java/tutorly/logic/parser/Parser.java index d6551ad8e3f..178f378dd67 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/tutorly/logic/parser/Parser.java @@ -1,7 +1,7 @@ -package seedu.address.logic.parser; +package tutorly.logic.parser; -import seedu.address.logic.commands.Command; -import seedu.address.logic.parser.exceptions.ParseException; +import tutorly.logic.commands.Command; +import tutorly.logic.parser.exceptions.ParseException; /** * Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}. diff --git a/src/main/java/tutorly/logic/parser/ParserUtil.java b/src/main/java/tutorly/logic/parser/ParserUtil.java new file mode 100644 index 00000000000..33a572e32af --- /dev/null +++ b/src/main/java/tutorly/logic/parser/ParserUtil.java @@ -0,0 +1,307 @@ +package tutorly.logic.parser; + +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.Collection; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import tutorly.commons.util.StringUtil; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.attendancerecord.Feedback; +import tutorly.model.person.Address; +import tutorly.model.person.Email; +import tutorly.model.person.Identity; +import tutorly.model.person.Memo; +import tutorly.model.person.Name; +import tutorly.model.person.Phone; +import tutorly.model.session.Session; +import tutorly.model.session.Subject; +import tutorly.model.session.Timeslot; +import tutorly.model.tag.Tag; + +/** + * Contains utility methods used for parsing strings in the various *Parser classes. + */ +public class ParserUtil { + + public static final String MESSAGE_INVALID_DATETIME = "Invalid datetime or incorrect datetime format. " + + "Please ensure it follows the format 'yyyy-MM-ddTHH:mm' (e.g. '2025-12-25T10:00') and is a valid " + + "datetime."; + public static final String MESSAGE_INVALID_DATE_FORMAT = "Invalid date or incorrect date format. " + + "Please ensure it follows the format 'dd MMM yyyy' (e.g. '25 Dec 2025') and is a valid date."; + public static final String MESSAGE_INVALID_TIMESLOT_FORMAT = "Invalid timeslot or incorrect timeslot format. " + + "Please ensure it follows the format 'dd MMM yyyy HH:mm-HH:mm' or 'dd MMM yyyy HH:mm-dd MMM yyyy HH:mm' " + + "(e.g. '25 Dec 2025 10:00-25 Dec 2025 12:00'), and the date and time provided is valid."; + public static final DateTimeFormatter DATE_FORMATTER = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendPattern("d MMM uuuu") + .toFormatter(Locale.ENGLISH) + .withResolverStyle(ResolverStyle.STRICT); + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("H:mm") + .withResolverStyle(ResolverStyle.STRICT); + + /** + * Parses {@code String identity} into an {@code Identity} and returns it. + * Leading and trailing whitespaces will be trimmed. + * Multiple intermediate spaces will be collapsed into one space. + * + * @throws ParseException if the specified identity is invalid (not non-zero unsigned integer or valid name). + */ + public static Identity parseIdentity(String identity) throws ParseException { + requireNonNull(identity); + String trimmedIdentity = identity.trim().replaceAll("\\s+", " "); + if (StringUtil.isNonZeroUnsignedInteger(trimmedIdentity)) { + if (!StringUtil.isParsableNonZeroUnsignedInteger(trimmedIdentity)) { + return new Identity(Identity.UNKNOWN_ID); + } + return new Identity(Integer.parseInt(trimmedIdentity)); + } + if (Name.isValidName(trimmedIdentity)) { + return new Identity(new Name(trimmedIdentity)); + } + throw new ParseException(Identity.MESSAGE_INVALID_IDENTITY); + } + + /** + * Parses Session {@code String id} into an {@code int} and returns it. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the specified id is invalid (not non-zero unsigned integer). + */ + public static int parseSessionId(String id) throws ParseException { + requireNonNull(id); + String trimmedId = id.trim(); + if (!StringUtil.isNonZeroUnsignedInteger(trimmedId)) { + throw new ParseException(Session.MESSAGE_INVALID_ID); + } + if (!StringUtil.isParsableNonZeroUnsignedInteger(trimmedId)) { + return Session.UNKNOWN_ID; + } + return Integer.parseInt(trimmedId); + } + + /** + * Parses a {@code String name} into a {@code Name}. + * Leading and trailing whitespaces will be trimmed. + * Multiple intermediate spaces will be collapsed into one space. + * + * @throws ParseException if the given {@code name} is invalid. + */ + public static Name parseName(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim().replaceAll("\\s+", " "); + if (!Name.isValidName(trimmedName)) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS); + } + return new Name(trimmedName); + } + + /** + * Parses a {@code String phone} into a {@code Phone}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code phone} is invalid. + */ + public static Phone parsePhone(String phone) throws ParseException { + requireNonNull(phone); + String trimmedPhone = phone.trim(); + if (!Phone.isValidPhone(trimmedPhone)) { + throw new ParseException(Phone.MESSAGE_CONSTRAINTS); + } + return new Phone(trimmedPhone); + } + + /** + * Parses a {@code String address} into an {@code Address}. + * Leading and trailing whitespaces will be trimmed. + * Multiple intermediate spaces will be collapsed into one space. + * + * @throws ParseException if the given {@code address} is invalid. + */ + public static Address parseAddress(String address) throws ParseException { + requireNonNull(address); + String trimmedAddress = address.trim().replaceAll("\\s+", " "); + if (!Address.isValidAddress(trimmedAddress)) { + throw new ParseException(Address.MESSAGE_CONSTRAINTS); + } + return new Address(trimmedAddress); + } + + /** + * Parses a {@code String email} into an {@code Email}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code email} is invalid. + */ + public static Email parseEmail(String email) throws ParseException { + requireNonNull(email); + String trimmedEmail = email.trim(); + if (!Email.isValidEmail(trimmedEmail)) { + throw new ParseException(Email.MESSAGE_CONSTRAINTS); + } + return new Email(trimmedEmail); + } + + /** + * Parses a {@code String tag} into a {@code Tag}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code tag} is invalid. + */ + public static Tag parseTag(String tag) throws ParseException { + requireNonNull(tag); + String trimmedTag = tag.trim(); + if (!Tag.isValidTagName(trimmedTag)) { + throw new ParseException(Tag.MESSAGE_CONSTRAINTS); + } + return new Tag(trimmedTag); + } + + /** + * Parses {@code Collection tags} into a {@code Set}. + */ + public static Set parseTags(Collection tags) throws ParseException { + requireNonNull(tags); + final Set tagSet = new HashSet<>(); + for (String tagName : tags) { + tagSet.add(parseTag(tagName)); + } + return tagSet; + } + + /** + * Parses a {@code String memo} into an {@code Memo}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code memo} is invalid. + */ + public static Memo parseMemo(String memo) throws ParseException { + requireNonNull(memo); + String trimmedMemo = memo.trim(); + if (!Memo.isValidMemo(trimmedMemo)) { + throw new ParseException(Memo.MESSAGE_CONSTRAINTS); + } + return new Memo(trimmedMemo); + } + + /** + * Parses a {@code String date} into a {@code LocalDate}. + * The date format must be d MMM yyyy. + * + * @param dateStr The date string to parse. + * @return The parsed LocalDate. + * @throws ParseException if the date format is invalid. + */ + public static LocalDate parseDate(String dateStr) throws ParseException { + requireNonNull(dateStr); + try { + return LocalDate.parse(dateStr.trim(), DATE_FORMATTER); + } catch (DateTimeParseException e) { + throw new ParseException(MESSAGE_INVALID_DATE_FORMAT); + } + } + + /** + * Parses a {@code String timeslot} into a {@code Timeslot}. + * The timeslot format must be d MMM yyyy H:mm-H:mm or d MMM yyyy H:mm-d MMM yyyy H:mm. + * + * @param timeslot The timeslot to parse. + * @return The parsed Timeslot. + * @throws ParseException if the date format is invalid. + */ + public static Timeslot parseTimeslot(String timeslot) throws ParseException { + requireNonNull(timeslot); + + // Split the timeslot into start and end times based on the first hyphen + String[] tokens = timeslot.trim().split("-"); + if (tokens.length != 2) { + throw new ParseException(MESSAGE_INVALID_TIMESLOT_FORMAT); + } + String startDateTimeStr = tokens[0].trim(); + String endDateTimeStr = tokens[1].trim(); + + // Process start datetime + String[] startTokens = startDateTimeStr.split("\\s+"); + if (startTokens.length != 4) { + throw new ParseException(MESSAGE_INVALID_TIMESLOT_FORMAT); + } + String startDateStr = startTokens[0] + " " + startTokens[1] + " " + startTokens[2]; + String startTimeStr = startTokens[3]; + + // Process end datetime + String[] endTokens = endDateTimeStr.split("\\s+"); + String endDateStr; + String endTimeStr; + if (endTokens.length == 1) { + endDateStr = startDateStr; + endTimeStr = endTokens[0]; + } else if (endTokens.length == 4) { + endDateStr = endTokens[0] + " " + endTokens[1] + " " + endTokens[2]; + endTimeStr = endTokens[3]; + } else { + throw new ParseException(MESSAGE_INVALID_TIMESLOT_FORMAT); + } + + LocalDate startDate; + LocalDate endDate; + LocalTime startTime; + LocalTime endTime; + try { + startDate = LocalDate.parse(startDateStr, DATE_FORMATTER); + endDate = LocalDate.parse(endDateStr, DATE_FORMATTER); + startTime = LocalTime.parse(startTimeStr, TIME_FORMATTER); + endTime = LocalTime.parse(endTimeStr, TIME_FORMATTER); + } catch (DateTimeParseException e) { + throw new ParseException(MESSAGE_INVALID_TIMESLOT_FORMAT); + } + + // Combine date and time into LocalDateTime objects + LocalDateTime startDateTime = LocalDateTime.of(startDate, startTime); + LocalDateTime endDateTime = LocalDateTime.of(endDate, endTime); + + if (!endDateTime.isAfter(startDateTime)) { + throw new ParseException(Timeslot.MESSAGE_END_BEFORE_START_DATETIME); + } + + return new Timeslot(startDateTime, endDateTime); + } + + /** + * 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. + */ + public static Subject parseSubject(String subject) throws ParseException { + requireNonNull(subject); + String trimmedSubject = subject.trim(); + if (!Subject.isValidSubject(trimmedSubject)) { + throw new ParseException(Subject.MESSAGE_CONSTRAINTS); + } + return new Subject(trimmedSubject); + } + + /** + * Parses a {@code String feedback} into a {@code Feedback}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code feedback} is invalid. + */ + public static Feedback parseFeedback(String feedback) throws ParseException { + requireNonNull(feedback); + String trimmedFeedback = feedback.trim(); + if (!Feedback.isValidFeedback(trimmedFeedback)) { + throw new ParseException(Feedback.MESSAGE_CONSTRAINTS); + } + return new Feedback(trimmedFeedback); + } +} diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/tutorly/logic/parser/Prefix.java similarity index 95% rename from src/main/java/seedu/address/logic/parser/Prefix.java rename to src/main/java/tutorly/logic/parser/Prefix.java index 348b7686c8a..a0054cf966f 100644 --- a/src/main/java/seedu/address/logic/parser/Prefix.java +++ b/src/main/java/tutorly/logic/parser/Prefix.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package tutorly.logic.parser; /** * A prefix that marks the beginning of an argument in an arguments string. diff --git a/src/main/java/tutorly/logic/parser/SearchSessionCommandParser.java b/src/main/java/tutorly/logic/parser/SearchSessionCommandParser.java new file mode 100644 index 00000000000..d5c0abda315 --- /dev/null +++ b/src/main/java/tutorly/logic/parser/SearchSessionCommandParser.java @@ -0,0 +1,65 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.CliSyntax.PREFIX_DATE; +import static tutorly.logic.parser.CliSyntax.PREFIX_NAME; +import static tutorly.logic.parser.CliSyntax.PREFIX_PHONE; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; +import static tutorly.logic.parser.CliSyntax.PREFIX_SUBJECT; +import static tutorly.logic.parser.ParserUtil.parseDate; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import tutorly.logic.commands.SearchSessionCommand; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.filter.DateSessionFilter; +import tutorly.model.filter.Filter; +import tutorly.model.filter.SubjectContainsKeywordsFilter; +import tutorly.model.session.Session; + +/** + * Parses input arguments and creates a new SearchSessionCommand object + */ +public class SearchSessionCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the SearchSessionCommand + * and returns a SearchSessionCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SearchSessionCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_DATE, PREFIX_SUBJECT); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_SESSION, PREFIX_NAME, PREFIX_PHONE); + + if (!argMultimap.getPreamble().isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchSessionCommand.MESSAGE_USAGE)); + } + + return new SearchSessionCommand(initFilter(argMultimap)); + } + + /** + * Initializes filter combining all predicates for filtering sessions using the given {@code ArgumentMultimap}. + */ + private static Filter initFilter(ArgumentMultimap argMultimap) throws ParseException { + List> filters = new ArrayList<>(); + + Optional dateQuery = argMultimap.getValue(PREFIX_DATE); + if (dateQuery.isPresent() && !dateQuery.get().isBlank()) { + LocalDate date = parseDate(dateQuery.get()); + filters.add(new DateSessionFilter(date)); + } + + Optional subjectQuery = argMultimap.getValue(PREFIX_SUBJECT); + if (subjectQuery.isPresent() && !subjectQuery.get().isBlank()) { + String[] subjectKeywords = subjectQuery.get().trim().split("\\s+"); + filters.add(new SubjectContainsKeywordsFilter(Arrays.asList(subjectKeywords))); + } + + return Filter.any(filters); + } +} diff --git a/src/main/java/tutorly/logic/parser/SearchStudentCommandParser.java b/src/main/java/tutorly/logic/parser/SearchStudentCommandParser.java new file mode 100644 index 00000000000..416bd3c5501 --- /dev/null +++ b/src/main/java/tutorly/logic/parser/SearchStudentCommandParser.java @@ -0,0 +1,70 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.CliSyntax.PREFIX_NAME; +import static tutorly.logic.parser.CliSyntax.PREFIX_PHONE; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; +import static tutorly.logic.parser.ParserUtil.parseSessionId; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import tutorly.logic.commands.SearchStudentCommand; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.filter.AttendSessionFilter; +import tutorly.model.filter.Filter; +import tutorly.model.filter.NameContainsKeywordsFilter; +import tutorly.model.filter.PhoneContainsKeywordsFilter; +import tutorly.model.person.Person; + +/** + * Parses input arguments and creates a new SearchCommand object + */ +public class SearchStudentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SearchCommand + * and returns a SearchCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SearchStudentCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SESSION, PREFIX_NAME, PREFIX_PHONE); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_SESSION, PREFIX_NAME, PREFIX_PHONE); + + if (!argMultimap.getPreamble().isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchStudentCommand.MESSAGE_USAGE)); + } + + return new SearchStudentCommand(initFilter(argMultimap)); + } + + /** + * Initializes filter combining all predicates for filtering persons using the given {@code ArgumentMultimap}. + */ + private static Filter initFilter(ArgumentMultimap argMultimap) throws ParseException { + List> filters = new ArrayList<>(); + + Optional sessionIdQuery = argMultimap.getValue(PREFIX_SESSION); + if (sessionIdQuery.isPresent() && !sessionIdQuery.get().isBlank()) { + int sessionId = parseSessionId(sessionIdQuery.get()); + filters.add(new AttendSessionFilter(sessionId)); + } + + Optional nameQuery = argMultimap.getValue(PREFIX_NAME); + if (nameQuery.isPresent() && !nameQuery.get().isBlank()) { + String[] nameKeywords = nameQuery.get().trim().split("\\s+"); + filters.add(new NameContainsKeywordsFilter(Arrays.asList(nameKeywords))); + } + + Optional phoneQuery = argMultimap.getValue(PREFIX_PHONE); + if (phoneQuery.isPresent() && !phoneQuery.get().isBlank()) { + String[] phoneKeywords = phoneQuery.get().trim().split("\\s+"); + filters.add(new PhoneContainsKeywordsFilter(Arrays.asList(phoneKeywords))); + } + + return Filter.any(filters); + } +} diff --git a/src/main/java/tutorly/logic/parser/SessionCommandParser.java b/src/main/java/tutorly/logic/parser/SessionCommandParser.java new file mode 100644 index 00000000000..4f05fbbe5d8 --- /dev/null +++ b/src/main/java/tutorly/logic/parser/SessionCommandParser.java @@ -0,0 +1,73 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_UNKNOWN_COMMAND; + +import tutorly.logic.commands.AddSessionCommand; +import tutorly.logic.commands.AttendanceFeedbackCommand; +import tutorly.logic.commands.AttendanceMarkSessionCommand; +import tutorly.logic.commands.AttendanceUnmarkSessionCommand; +import tutorly.logic.commands.Command; +import tutorly.logic.commands.DeleteSessionCommand; +import tutorly.logic.commands.EditSessionCommand; +import tutorly.logic.commands.EnrolSessionCommand; +import tutorly.logic.commands.ListSessionCommand; +import tutorly.logic.commands.SearchSessionCommand; +import tutorly.logic.commands.SessionCommand; +import tutorly.logic.commands.UnenrolSessionCommand; +import tutorly.logic.commands.ViewSessionCommand; +import tutorly.logic.parser.exceptions.ParseException; + +/** + * Subparser for the session command. + */ +public class SessionCommandParser extends AddressBookParser { + + @Override + protected Command parseCommand(String command, String args) throws ParseException { + command = command.toLowerCase(); + + switch (command) { + case ViewSessionCommand.COMMAND_WORD: + return new ViewSessionCommandParser().parse(args); + + case ListSessionCommand.COMMAND_WORD: + return new ListSessionCommand(); + + case AddSessionCommand.COMMAND_WORD: + return new AddSessionCommandParser().parse(args); + + case SearchSessionCommand.COMMAND_WORD: + return new SearchSessionCommandParser().parse(args); + + case EnrolSessionCommand.COMMAND_WORD: + return new EnrolSessionCommandParser().parse(args); + + case UnenrolSessionCommand.COMMAND_WORD: + return new UnenrolSessionCommandParser().parse(args); + + case DeleteSessionCommand.COMMAND_WORD: + return new DeleteSessionCommandParser().parse(args); + + case EditSessionCommand.COMMAND_WORD: + return new EditSessionCommandParser().parse(args); + + case AttendanceMarkSessionCommand.COMMAND_WORD: + return new AttendanceMarkSessionCommandParser().parse(args); + + case AttendanceUnmarkSessionCommand.COMMAND_WORD: + return new AttendanceUnmarkSessionCommandParser().parse(args); + + case AttendanceFeedbackCommand.COMMAND_WORD: + return new AttendanceFeedbackCommandParser().parse(args); + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + + @Override + protected Command defaultCommand() { + return new SessionCommand(); + } + +} diff --git a/src/main/java/tutorly/logic/parser/StudentCommandParser.java b/src/main/java/tutorly/logic/parser/StudentCommandParser.java new file mode 100644 index 00000000000..a682850ce5d --- /dev/null +++ b/src/main/java/tutorly/logic/parser/StudentCommandParser.java @@ -0,0 +1,53 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_UNKNOWN_COMMAND; + +import tutorly.logic.commands.AddStudentCommand; +import tutorly.logic.commands.Command; +import tutorly.logic.commands.DeleteStudentCommand; +import tutorly.logic.commands.EditStudentCommand; +import tutorly.logic.commands.ListStudentCommand; +import tutorly.logic.commands.SearchStudentCommand; +import tutorly.logic.commands.StudentCommand; +import tutorly.logic.commands.ViewStudentCommand; +import tutorly.logic.parser.exceptions.ParseException; + +/** + * Subparser for the student command. + */ +public class StudentCommandParser extends AddressBookParser { + + @Override + protected Command parseCommand(String command, String args) throws ParseException { + command = command.toLowerCase(); + + switch (command) { + case ViewStudentCommand.COMMAND_WORD: + return new ViewStudentCommandParser().parse(args); + + case ListStudentCommand.COMMAND_WORD: + return new ListStudentCommand(); + + case AddStudentCommand.COMMAND_WORD: + return new AddStudentCommandParser().parse(args); + + case EditStudentCommand.COMMAND_WORD: + return new EditStudentCommandParser().parse(args); + + case DeleteStudentCommand.COMMAND_WORD: + return new DeleteStudentCommandParser().parse(args); + + case SearchStudentCommand.COMMAND_WORD: + return new SearchStudentCommandParser().parse(args); + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + + @Override + protected Command defaultCommand() { + return new StudentCommand(); + } + +} diff --git a/src/main/java/tutorly/logic/parser/UnenrolSessionCommandParser.java b/src/main/java/tutorly/logic/parser/UnenrolSessionCommandParser.java new file mode 100644 index 00000000000..891c10099b1 --- /dev/null +++ b/src/main/java/tutorly/logic/parser/UnenrolSessionCommandParser.java @@ -0,0 +1,36 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.CliSyntax.PREFIX_SESSION; +import static tutorly.logic.parser.ParserUtil.parseSessionId; + +import java.util.Optional; + +import tutorly.logic.commands.UnenrolSessionCommand; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.person.Identity; + +/** + * Parses input arguments and creates a new UnenrolSessionCommand object + */ +public class UnenrolSessionCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the UnenrolSessionCommand + * and returns a UnenrolSessionCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public UnenrolSessionCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SESSION); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_SESSION); + Optional sessionId = argMultimap.getValue(PREFIX_SESSION); + + if (sessionId.isEmpty() || sessionId.get().isBlank() || argMultimap.getPreamble().isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnenrolSessionCommand.MESSAGE_USAGE)); + } + + Identity identity = ParserUtil.parseIdentity(argMultimap.getPreamble()); + return new UnenrolSessionCommand(identity, parseSessionId(sessionId.get())); + } +} diff --git a/src/main/java/tutorly/logic/parser/ViewSessionCommandParser.java b/src/main/java/tutorly/logic/parser/ViewSessionCommandParser.java new file mode 100644 index 00000000000..65fa5dfdaac --- /dev/null +++ b/src/main/java/tutorly/logic/parser/ViewSessionCommandParser.java @@ -0,0 +1,29 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static tutorly.logic.parser.ParserUtil.parseSessionId; + +import tutorly.logic.commands.ViewSessionCommand; +import tutorly.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ViewSessionCommand object + */ +public class ViewSessionCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ViewSessionCommand + * and returns a ViewSessionCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + public ViewSessionCommand parse(String args) throws ParseException { + try { + int sessionId = parseSessionId(args.trim()); + return new ViewSessionCommand(sessionId); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewSessionCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/tutorly/logic/parser/ViewStudentCommandParser.java b/src/main/java/tutorly/logic/parser/ViewStudentCommandParser.java new file mode 100644 index 00000000000..ec93b088cd5 --- /dev/null +++ b/src/main/java/tutorly/logic/parser/ViewStudentCommandParser.java @@ -0,0 +1,30 @@ +package tutorly.logic.parser; + +import static tutorly.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import tutorly.logic.commands.ViewStudentCommand; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.person.Identity; + +/** + * Parses input arguments and creates a new ViewStudentCommand object + */ +public class ViewStudentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ViewStudentCommand + * and returns a ViewStudentCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public ViewStudentCommand parse(String args) throws ParseException { + try { + Identity identity = ParserUtil.parseIdentity(args); + return new ViewStudentCommand(identity); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewStudentCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java b/src/main/java/tutorly/logic/parser/exceptions/ParseException.java similarity index 73% rename from src/main/java/seedu/address/logic/parser/exceptions/ParseException.java rename to src/main/java/tutorly/logic/parser/exceptions/ParseException.java index 158a1a54c1c..7d9f0cb996d 100644 --- a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java +++ b/src/main/java/tutorly/logic/parser/exceptions/ParseException.java @@ -1,6 +1,6 @@ -package seedu.address.logic.parser.exceptions; +package tutorly.logic.parser.exceptions; -import seedu.address.commons.exceptions.IllegalValueException; +import tutorly.commons.exceptions.IllegalValueException; /** * Represents a parse error encountered by a parser. diff --git a/src/main/java/tutorly/model/AddressBook.java b/src/main/java/tutorly/model/AddressBook.java new file mode 100644 index 00000000000..3c08603ed18 --- /dev/null +++ b/src/main/java/tutorly/model/AddressBook.java @@ -0,0 +1,323 @@ +package tutorly.model; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import javafx.collections.ObservableList; +import tutorly.commons.util.ToStringBuilder; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.attendancerecord.UniqueAttendanceRecordList; +import tutorly.model.person.Name; +import tutorly.model.person.Person; +import tutorly.model.person.UniquePersonList; +import tutorly.model.session.Session; +import tutorly.model.session.UniqueSessionList; + +/** + * Wraps all data at the address-book level. + * Duplicates are not allowed. + */ +public class AddressBook implements ReadOnlyAddressBook { + + private final UniquePersonList persons; + private final UniqueSessionList sessions; + private final UniqueAttendanceRecordList attendanceRecords; + + private int nextPersonId; + private int nextSessionId; + + /** + * Creates an AddressBook. + */ + public AddressBook() { + persons = new UniquePersonList(); + sessions = new UniqueSessionList(); + attendanceRecords = new UniqueAttendanceRecordList(); + + nextPersonId = 1; + nextSessionId = 1; + } + + /** + * Creates an AddressBook. + */ + public AddressBook(int nextPersonId, int nextSessionId) { + this(); + + this.nextPersonId = nextPersonId; + this.nextSessionId = nextSessionId; + } + + /** + * Creates an AddressBook using the ReadOnlyAddressBook in the {@code toBeCopied} + */ + public AddressBook(ReadOnlyAddressBook toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the person list with {@code persons}. + * {@code persons} must not contain duplicate persons. + */ + public void setPersons(List persons) { + this.persons.setAll(persons); + } + + /** + * Replaces the contents of the session list with {@code sessions}. + * {@code sessions} must not contain duplicate sessions. + */ + public void setSessions(List sessions) { + this.sessions.setAll(sessions); + } + + /** + * Replaces the contents of the attendance records list with {@code attendanceRecords}. + */ + public void setAttendanceRecords(List attendanceRecords) { + this.attendanceRecords.setAll(attendanceRecords); + } + + /** + * Resets the existing data of this {@code AddressBook} with {@code newData}. + */ + public void resetData(ReadOnlyAddressBook newData) { + requireNonNull(newData); + + setPersons(newData.getPersonList()); + setSessions(newData.getSessionList()); + setAttendanceRecords(newData.getAttendanceRecordsList()); + + nextPersonId = newData.getNextPersonId(); + nextSessionId = newData.getNextSessionId(); + } + + //// person-level operations + + /** + * Returns true if a person with the same identity as {@code person} exists in the address book. + */ + public boolean hasPerson(Person person) { + requireNonNull(person); + return persons.contains(person); + } + + /** + * Adds a person to the address book. + * The person must not already exist in the address book. + */ + public void addPerson(Person p) { + if (p.getId() == 0) { + // Set the student ID of the person if it has not been set + if (nextPersonId >= Integer.MAX_VALUE) { + throw new IllegalStateException(); + } + + p.setId(nextPersonId++); + } else if (p.getId() >= nextPersonId) { + nextPersonId = p.getId() + 1; + } + + persons.add(p); + } + + /** + * Returns the person with the given ID if it exists in the persons address book. + */ + public Optional getPersonById(int id) { + return persons.getPersonById(id); + } + + /** + * Returns the person with the given name if it exists in the persons address book. + */ + public Optional getPersonByName(Name name) { + return persons.getPersonByName(name); + } + + /** + * Replaces the given person {@code target} in the list with {@code editedPerson}. + * {@code target} must exist in the address book. + * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + */ + public void setPerson(Person target, Person editedPerson) { + requireNonNull(editedPerson); + persons.set(target, editedPerson); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the address book. + */ + public void removePerson(Person key) { + persons.remove(key); + } + + //// session-level operations + + /** + * Returns true if a session with the same identity as {@code toCheck} exists in the address book. + */ + public boolean hasSession(Session toCheck) { + requireNonNull(toCheck); + return sessions.contains(toCheck); + } + + /** + * Returns true if the session {@code toCheck} overlaps with any existing sessions in the address book. + */ + public boolean hasOverlappingSession(Session toCheck) { + requireNonNull(toCheck); + return sessions.hasOverlappingSession(toCheck); + } + + /** + * Adds a session to the address book. + * The session must not already exist in the address book. + */ + public void addSession(Session s) { + if (s.getId() == 0) { + // Set the session ID of the session if it has not been set + if (nextSessionId >= Integer.MAX_VALUE) { + throw new IllegalStateException(); + } + + s.setId(nextSessionId++); + } else if (s.getId() >= nextSessionId) { + nextSessionId = s.getId() + 1; + } + + sessions.add(s); + } + + /** + * Returns the person with the given ID if it exists in the address book. + */ + public Optional getSessionById(int id) { + return sessions.getSessionById(id); + } + + /** + * Replaces the given session {@code target} in the list with {@code editedSession}. + * {@code target} must exist in the address book. + * The session identity of {@code editedSession} must not be the same as another session in the address book. + */ + public void setSession(Session target, Session editedSession) { + requireNonNull(editedSession); + sessions.set(target, editedSession); + } + + /** + * Removes {@code session} from this {@code AddressBook}. + * {@code session} must exist in the address book. + */ + public void removeSession(Session session) { + sessions.remove(session); + } + + //// attendance record-level operations + + /** + * Returns true if an equivalent attendance record as {@code attendanceRecord} exists in the address book. + */ + public boolean hasAttendanceRecord(AttendanceRecord attendanceRecord) { + requireNonNull(attendanceRecord); + return attendanceRecords.contains(attendanceRecord); + } + + /** + * Returns the attendance record equivalent to the given record. + */ + public Optional findAttendanceRecord(AttendanceRecord attendanceRecord) { + requireNonNull(attendanceRecord); + return attendanceRecords.find(attendanceRecord); + } + + /** + * Adds an attendance record to the address book. + */ + public void addAttendanceRecord(AttendanceRecord attendanceRecord) { + attendanceRecords.add(attendanceRecord); + } + + /** + * Replaces the given attendance record {@code target} in the list with {@code editedAttendanceRecord}. + * {@code target} must exist in the address book. + */ + public void setAttendanceRecord(AttendanceRecord target, AttendanceRecord editedAttendanceRecord) { + requireNonNull(editedAttendanceRecord); + + attendanceRecords.set(target, editedAttendanceRecord); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the address book. + */ + public void removeAttendanceRecord(AttendanceRecord key) { + attendanceRecords.remove(key); + } + + //// util methods + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("persons", persons) + .add("sessions", sessions) + .add("attendanceRecords", attendanceRecords) + .toString(); + } + + @Override + public ObservableList getPersonList() { + return persons.asUnmodifiableObservableList(); + } + + @Override + public ObservableList getSessionList() { + return sessions.asUnmodifiableObservableList(); + } + + @Override + public ObservableList getAttendanceRecordsList() { + return attendanceRecords.asUnmodifiableObservableList(); + } + + @Override + public int getNextPersonId() { + return nextPersonId; + } + + @Override + public int getNextSessionId() { + return nextSessionId; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddressBook otherAddressBook)) { + return false; + } + + return persons.equals(otherAddressBook.persons) + && sessions.equals(otherAddressBook.sessions) + && attendanceRecords.equals(otherAddressBook.attendanceRecords); + } + + @Override + public int hashCode() { + return Objects.hash(persons, sessions, attendanceRecords, nextPersonId, nextSessionId); + } +} diff --git a/src/main/java/tutorly/model/Model.java b/src/main/java/tutorly/model/Model.java new file mode 100644 index 00000000000..e6003779116 --- /dev/null +++ b/src/main/java/tutorly/model/Model.java @@ -0,0 +1,209 @@ +package tutorly.model; + +import java.nio.file.Path; +import java.util.Optional; + +import javafx.collections.ObservableList; +import tutorly.commons.core.GuiSettings; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.filter.Filter; +import tutorly.model.person.Identity; +import tutorly.model.person.Name; +import tutorly.model.person.Person; +import tutorly.model.session.Session; + +/** + * The API of the Model component. + */ +public interface Model { + /** + * {@code Filter} that always evaluate to true + */ + Filter FILTER_SHOW_ALL_PERSONS = ab -> p -> true; + + /** + * {@code Filter} that always evaluate to true + */ + Filter FILTER_SHOW_ALL_SESSIONS = ab -> s -> true; + + /** + * Returns the user prefs. + */ + ReadOnlyUserPrefs getUserPrefs(); + + /** + * Replaces user prefs data with the data in {@code userPrefs}. + */ + void setUserPrefs(ReadOnlyUserPrefs userPrefs); + + /** + * Returns the user prefs' GUI settings. + */ + GuiSettings getGuiSettings(); + + /** + * Sets the user prefs' GUI settings. + */ + void setGuiSettings(GuiSettings guiSettings); + + /** + * Returns the user prefs' address book file path. + */ + Path getAddressBookFilePath(); + + /** + * Sets the user prefs' address book file path. + */ + void setAddressBookFilePath(Path addressBookFilePath); + + /** + * Returns the AddressBook + */ + ReadOnlyAddressBook getAddressBook(); + + /** + * Replaces address book data with the data in {@code addressBook}. + */ + void setAddressBook(ReadOnlyAddressBook addressBook); + + /** + * Returns true if a person with the same identity as {@code person} exists in the address book. + */ + boolean hasPerson(Person person); + + /** + * Deletes the given person. + * The person must exist in the address book. + */ + void deletePerson(Person target); + + /** + * Adds the given person. + * {@code person} must not already exist in the address book. + */ + void addPerson(Person person); + + /** + * Replaces the given person {@code target} with {@code editedPerson}. + * {@code target} must exist in the address book. + * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + */ + void setPerson(Person target, Person editedPerson); + + /** + * Returns an optional of the person with the given id. + */ + Optional getPersonById(int id); + + /** + * Returns an optional of the person with the given name. + */ + Optional getPersonByName(Name name); + + /** + * Returns an optional of the person with the given identity consisting of either ID or name. + */ + Optional getPersonByIdentity(Identity identity); + + /** + * Returns an unmodifiable view of the person list + */ + ObservableList getPersonList(); + + /** + * Returns an unmodifiable view of the filtered person list + */ + ObservableList getFilteredPersonList(); + + /** + * Returns an unmodifiable view of the session list + */ + ObservableList getSessionList(); + + /** + * Returns an unmodifiable view of the filtered session list + */ + ObservableList getFilteredSessionList(); + + /** + * Returns an unmodifiable view of the attendance record list + */ + ObservableList getAttendanceRecordList(); + + /** + * Updates the filter of the filtered person list to filter by the given {@code filter}. + * + * @throws NullPointerException if {@code filter} is null. + */ + void updateFilteredPersonList(Filter filter); + + /** + * Updates the filter of the filtered session list to filter by the given {@code filter}. + * + * @throws NullPointerException if {@code filter} is null. + */ + void updateFilteredSessionList(Filter filter); + + /** + * Returns true if a session with the same identity as {@code session} exists in the address book. + */ + boolean hasSession(Session toCreate); + + /** + * Returns true if the session to be created has overlapping timeslot with an existing session. + */ + boolean hasOverlappingSession(Session toCreate); + + /** + * Adds the given session. + * {@code session} must not already exist in the address book. + */ + void addSession(Session toCreate); + + /** + * Deletes the given session. + * {@code session} must already exist in the address book. + */ + void deleteSession(Session target); + + /** + * Replaces the given session {@code session} with {@code editedSession}. + * {@code session} must exist in the address book. + */ + void setSession(Session session, Session editedSession); + + + /** + * Returns an optional of the session with the given id. + */ + Optional getSessionById(int id); + + /** + * Returns true if an AttendanceRecord with the same identity as + * {@code record} exists in the address book. + */ + boolean hasAttendanceRecord(AttendanceRecord record); + + /** + * Returns the attendance record equivalent to the given record. + */ + Optional findAttendanceRecord(AttendanceRecord record); + + /** + * Adds the given AttendanceRecord. + * {@code record} must not already exist in the address book. + */ + void addAttendanceRecord(AttendanceRecord record); + + /** + * Deletes the given AttendanceRecord. + * {@code record} must already exist in the address book. + */ + void removeAttendanceRecord(AttendanceRecord record); + + /* Replaces the given AttendanceRecord {@code target} with {@code editedRecord}. + * {@code target} must exist in the address book. + * The AttendanceRecord {@code editedRecord} must not be equivalent to another existing record. + */ + void setAttendanceRecord(AttendanceRecord target, AttendanceRecord editedRecord); +} diff --git a/src/main/java/tutorly/model/ModelManager.java b/src/main/java/tutorly/model/ModelManager.java new file mode 100644 index 00000000000..abb1f501b52 --- /dev/null +++ b/src/main/java/tutorly/model/ModelManager.java @@ -0,0 +1,262 @@ +package tutorly.model; + +import static java.util.Objects.requireNonNull; +import static tutorly.commons.util.CollectionUtil.requireAllNonNull; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import tutorly.commons.core.GuiSettings; +import tutorly.commons.core.LogsCenter; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.filter.Filter; +import tutorly.model.person.Identity; +import tutorly.model.person.Name; +import tutorly.model.person.Person; +import tutorly.model.session.Session; + +/** + * Represents the in-memory model of the address book data. + */ +public class ModelManager implements Model { + private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + + private final AddressBook addressBook; + private final UserPrefs userPrefs; + private final FilteredList filteredPersons; + private final FilteredList filteredSessions; + + /** + * Initializes a ModelManager with the given addressBook and userPrefs. + */ + public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { + requireAllNonNull(addressBook, userPrefs); + + logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); + + this.addressBook = new AddressBook(addressBook); + this.userPrefs = new UserPrefs(userPrefs); + filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredSessions = new FilteredList<>(this.addressBook.getSessionList()); + } + + public ModelManager() { + this(new AddressBook(), new UserPrefs()); + } + + //=========== UserPrefs ================================================================================== + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + return userPrefs; + } + + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + requireNonNull(userPrefs); + this.userPrefs.resetData(userPrefs); + } + + @Override + public GuiSettings getGuiSettings() { + return userPrefs.getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + requireNonNull(guiSettings); + userPrefs.setGuiSettings(guiSettings); + } + + @Override + public Path getAddressBookFilePath() { + return userPrefs.getAddressBookFilePath(); + } + + @Override + public void setAddressBookFilePath(Path addressBookFilePath) { + requireNonNull(addressBookFilePath); + userPrefs.setAddressBookFilePath(addressBookFilePath); + } + + //=========== AddressBook ================================================================================ + + @Override + public ReadOnlyAddressBook getAddressBook() { + return addressBook; + } + + @Override + public void setAddressBook(ReadOnlyAddressBook addressBook) { + this.addressBook.resetData(addressBook); + } + + @Override + public boolean hasPerson(Person person) { + requireNonNull(person); + return addressBook.hasPerson(person); + } + + @Override + public void deletePerson(Person target) { + addressBook.removePerson(target); + } + + @Override + public void addPerson(Person person) { + addressBook.addPerson(person); + } + + @Override + public void setPerson(Person target, Person editedPerson) { + requireAllNonNull(target, editedPerson); + + addressBook.setPerson(target, editedPerson); + } + + @Override + public Optional getPersonById(int id) { + return addressBook.getPersonById(id); + } + + @Override + public Optional getPersonByName(Name name) { + return addressBook.getPersonByName(name); + } + + @Override + public Optional getPersonByIdentity(Identity identity) { + if (identity.isIdPresent()) { + return getPersonById(identity.getId()); + } else if (identity.isNamePresent()) { + return getPersonByName(identity.getName()); + } + + return Optional.empty(); + } + + //=========== Filtered Person List Accessors ============================================================= + + @Override + public ObservableList getPersonList() { + return addressBook.getPersonList(); + } + + @Override + public ObservableList getFilteredPersonList() { + return filteredPersons; + } + + @Override + public ObservableList getSessionList() { + return addressBook.getSessionList(); + } + + @Override + public ObservableList getFilteredSessionList() { + return filteredSessions; + } + + @Override + public ObservableList getAttendanceRecordList() { + return addressBook.getAttendanceRecordsList(); + } + + @Override + public void updateFilteredPersonList(Filter filter) { + requireNonNull(filter); + filteredPersons.setPredicate(filter.toPredicate(getAddressBook())); + } + + @Override + public void updateFilteredSessionList(Filter filter) { + requireNonNull(filter); + filteredSessions.setPredicate(filter.toPredicate(getAddressBook())); + } + + @Override + public boolean hasSession(Session toCreate) { + requireAllNonNull(toCreate); + return addressBook.hasSession(toCreate); + } + + @Override + public boolean hasOverlappingSession(Session toCreate) { + requireAllNonNull(toCreate); + return addressBook.hasOverlappingSession(toCreate); + } + + @Override + public void addSession(Session toCreate) { + requireAllNonNull(toCreate); + addressBook.addSession(toCreate); + } + + @Override + public void deleteSession(Session target) { + addressBook.removeSession(target); + } + + @Override + public void setSession(Session target, Session editedSession) { + requireAllNonNull(target, editedSession); + addressBook.setSession(target, editedSession); + } + + @Override + public Optional getSessionById(int id) { + return addressBook.getSessionById(id); + } + + @Override + public boolean hasAttendanceRecord(AttendanceRecord record) { + requireNonNull(record); + return addressBook.hasAttendanceRecord(record); + } + + @Override + public Optional findAttendanceRecord(AttendanceRecord record) { + requireNonNull(record); + return addressBook.findAttendanceRecord(record); + } + + @Override + public void addAttendanceRecord(AttendanceRecord record) { + requireNonNull(record); + addressBook.addAttendanceRecord(record); + } + + @Override + public void removeAttendanceRecord(AttendanceRecord record) { + requireNonNull(record); + addressBook.removeAttendanceRecord(record); + } + + @Override + public void setAttendanceRecord(AttendanceRecord target, AttendanceRecord editedRecord) { + requireAllNonNull(target, editedRecord); + + addressBook.setAttendanceRecord(target, editedRecord); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ModelManager otherModelManager)) { + return false; + } + + return addressBook.equals(otherModelManager.addressBook) + && userPrefs.equals(otherModelManager.userPrefs) + && filteredPersons.equals(otherModelManager.filteredPersons) + && filteredSessions.equals(otherModelManager.filteredSessions); + } + +} diff --git a/src/main/java/tutorly/model/ReadOnlyAddressBook.java b/src/main/java/tutorly/model/ReadOnlyAddressBook.java new file mode 100644 index 00000000000..1c215e4a56b --- /dev/null +++ b/src/main/java/tutorly/model/ReadOnlyAddressBook.java @@ -0,0 +1,39 @@ +package tutorly.model; + +import javafx.collections.ObservableList; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.person.Person; +import tutorly.model.session.Session; + +/** + * Unmodifiable view of an address book + */ +public interface ReadOnlyAddressBook { + + /** + * Returns an unmodifiable view of the persons list. + * This list will not contain any duplicate persons. + */ + ObservableList getPersonList(); + + /** + * Returns an unmodifiable view of the sessions list. + */ + ObservableList getSessionList(); + + /** + * Returns an unmodifiable view of the attendance records list. + */ + ObservableList getAttendanceRecordsList(); + + /** + * Returns the next person ID. + */ + int getNextPersonId(); + + /** + * Returns the next session ID. + */ + int getNextSessionId(); + +} diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/tutorly/model/ReadOnlyUserPrefs.java similarity index 70% rename from src/main/java/seedu/address/model/ReadOnlyUserPrefs.java rename to src/main/java/tutorly/model/ReadOnlyUserPrefs.java index befd58a4c73..d00218b0f8d 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/tutorly/model/ReadOnlyUserPrefs.java @@ -1,8 +1,8 @@ -package seedu.address.model; +package tutorly.model; import java.nio.file.Path; -import seedu.address.commons.core.GuiSettings; +import tutorly.commons.core.GuiSettings; /** * Unmodifiable view of user prefs. diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/tutorly/model/UserPrefs.java similarity index 93% rename from src/main/java/seedu/address/model/UserPrefs.java rename to src/main/java/tutorly/model/UserPrefs.java index 6be655fb4c7..f860d87622f 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/tutorly/model/UserPrefs.java @@ -1,4 +1,4 @@ -package seedu.address.model; +package tutorly.model; import static java.util.Objects.requireNonNull; @@ -6,7 +6,7 @@ import java.nio.file.Paths; import java.util.Objects; -import seedu.address.commons.core.GuiSettings; +import tutorly.commons.core.GuiSettings; /** * Represents User's preferences. @@ -14,7 +14,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path addressBookFilePath = Paths.get("data" , "tutorly.json"); /** * Creates a {@code UserPrefs} with default values. diff --git a/src/main/java/tutorly/model/attendancerecord/AttendanceRecord.java b/src/main/java/tutorly/model/attendancerecord/AttendanceRecord.java new file mode 100644 index 00000000000..a18803502e0 --- /dev/null +++ b/src/main/java/tutorly/model/attendancerecord/AttendanceRecord.java @@ -0,0 +1,91 @@ +package tutorly.model.attendancerecord; + +import static java.util.Objects.requireNonNull; + +import tutorly.commons.util.ToStringBuilder; + +/** + * Represents the attendance record of a student. + */ +public class AttendanceRecord { + + public static final String MESSAGE_CONSTRAINTS = "Attendance record must have a valid student ID and session ID."; + + private int studentId; + private int sessionId; + private boolean isPresent; + private Feedback feedback; + + /** + * Constructs a new AttendanceRecord. + * + * @param studentId The ID of the student whose attendance is being recorded. + * @param sessionId The ID of the session for which attendance is being recorded. + * @param isPresent Whether the student is present for the session. + * @param feedback The feedback given by the student for the session. + */ + public AttendanceRecord(int studentId, int sessionId, boolean isPresent, Feedback feedback) { + requireNonNull(feedback); + this.studentId = studentId; + this.sessionId = sessionId; + this.isPresent = isPresent; + this.feedback = feedback; + } + + public int getStudentId() { + return studentId; + } + + public int getSessionId() { + return sessionId; + } + + public boolean getAttendance() { + return isPresent; + } + + public Feedback getFeedback() { + return feedback; + } + + /** + * Returns true if the record is for the same student and session. + * This defines a weaker notion of equality between two attendance records. + */ + public boolean isSameRecord(AttendanceRecord otherRecord) { + if (otherRecord == this) { + return true; + } + + return otherRecord != null + && studentId == otherRecord.studentId + && sessionId == otherRecord.sessionId; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AttendanceRecord)) { + return false; + } + + AttendanceRecord otherRecord = (AttendanceRecord) other; + return studentId == otherRecord.studentId + && sessionId == otherRecord.sessionId + && isPresent == otherRecord.isPresent + && feedback.equals(otherRecord.feedback); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("studentId", studentId) + .add("sessionId", sessionId) + .add("isPresent", isPresent) + .add("feedback", feedback) + .toString(); + } +} diff --git a/src/main/java/tutorly/model/attendancerecord/Feedback.java b/src/main/java/tutorly/model/attendancerecord/Feedback.java new file mode 100644 index 00000000000..5b945b9391c --- /dev/null +++ b/src/main/java/tutorly/model/attendancerecord/Feedback.java @@ -0,0 +1,78 @@ +package tutorly.model.attendancerecord; + +import static java.util.Objects.requireNonNull; +import static tutorly.commons.util.AppUtil.checkArgument; + +/** + * Represents a feedback in AttendanceRecord in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidFeedback(String)} + */ +public class Feedback { + public static final int MAX_LENGTH = 200; + + public static final String MESSAGE_CONSTRAINTS = "Feedback can take any values. " + + "The maximum length is " + MAX_LENGTH + " characters."; + + /* + * The first character of the feedback must not be a whitespace. Newlines are allowed. + * Fully empty strings are allowed too. + */ + public static final String VALIDATION_REGEX = "^$|[^\\s](?s).*"; + + private static final Feedback EMPTY_FEEDBACK = new Feedback(); + + public final String value; + + /** + * Constructs an empty {@code Feedback} instance. + */ + private Feedback() { + value = ""; + } + + /** + * Constructs a {@code Feedback}. + * + * @param feedback A valid feedback. + */ + public Feedback(String feedback) { + requireNonNull(feedback); + checkArgument(isValidFeedback(feedback), MESSAGE_CONSTRAINTS); + value = feedback; + } + + /** + * Returns an empty Feedback instance. + */ + public static Feedback empty() { + return EMPTY_FEEDBACK; + } + + /** + * Returns true if a given string is a valid feedback. + */ + public static boolean isValidFeedback(String test) { + return test.matches(VALIDATION_REGEX) && test.length() <= MAX_LENGTH; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Feedback otherFeedback)) { + return false; + } + return value.equals(otherFeedback.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/tutorly/model/attendancerecord/UniqueAttendanceRecordList.java b/src/main/java/tutorly/model/attendancerecord/UniqueAttendanceRecordList.java new file mode 100644 index 00000000000..2bbf18bbb19 --- /dev/null +++ b/src/main/java/tutorly/model/attendancerecord/UniqueAttendanceRecordList.java @@ -0,0 +1,23 @@ +package tutorly.model.attendancerecord; + +import tutorly.model.uniquelist.UniqueList; + +/** + * A list of attendance records that enforces uniqueness between its elements and does not allow nulls. + * An attendance record is considered unique by comparing using {@code AttendanceRecord#isSameRecord(AttendanceRecord)}. + * + * @see AttendanceRecord#isSameRecord(AttendanceRecord) + */ +public class UniqueAttendanceRecordList extends UniqueList { + + @Override + protected boolean isEquivalent(AttendanceRecord a, AttendanceRecord b) { + return a.isSameRecord(b); + } + + @Override + protected int compare(AttendanceRecord a, AttendanceRecord b) { + return Integer.compare(a.getStudentId(), b.getStudentId()); + } + +} diff --git a/src/main/java/tutorly/model/filter/AnyFilter.java b/src/main/java/tutorly/model/filter/AnyFilter.java new file mode 100644 index 00000000000..89ebceb2af7 --- /dev/null +++ b/src/main/java/tutorly/model/filter/AnyFilter.java @@ -0,0 +1,44 @@ +package tutorly.model.filter; + +import java.util.List; +import java.util.function.Predicate; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.model.ReadOnlyAddressBook; + +/** + * Represents a filter that represents the logical OR of the given filters. + */ +public class AnyFilter implements Filter { + + private final List> filters; + + protected AnyFilter(List> filters) { + this.filters = filters; + } + + @Override + public Predicate toPredicate(ReadOnlyAddressBook addressBook) { + return filters.stream().map(f -> f.toPredicate(addressBook)).reduce(Predicate::or).orElse(t -> true); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AnyFilter otherAnyFilter)) { + return false; + } + + return filters.equals(otherAnyFilter.filters); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("filters", filters).toString(); + } + +} diff --git a/src/main/java/tutorly/model/filter/AttendSessionFilter.java b/src/main/java/tutorly/model/filter/AttendSessionFilter.java new file mode 100644 index 00000000000..129fe20c50c --- /dev/null +++ b/src/main/java/tutorly/model/filter/AttendSessionFilter.java @@ -0,0 +1,51 @@ +package tutorly.model.filter; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.function.Predicate; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.person.Person; + +/** + * Represents a filter for a {@code Person} who attends a session with the given session ID. + */ +public class AttendSessionFilter implements Filter { + private final int sessionId; + + public AttendSessionFilter(int sessionId) { + this.sessionId = sessionId; + } + + @Override + public Predicate toPredicate(ReadOnlyAddressBook addressBook) { + requireNonNull(addressBook); + + List filteredAttendanceRecords = + addressBook.getAttendanceRecordsList().filtered(record -> record.getSessionId() == sessionId); + return person -> filteredAttendanceRecords.stream().anyMatch(record -> record.getStudentId() == person.getId()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AttendSessionFilter otherAttendSessionFilter)) { + return false; + } + + return sessionId == otherAttendSessionFilter.sessionId; + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("sessionId", sessionId).toString(); + } + +} diff --git a/src/main/java/tutorly/model/filter/DateSessionFilter.java b/src/main/java/tutorly/model/filter/DateSessionFilter.java new file mode 100644 index 00000000000..0d3fb0cc157 --- /dev/null +++ b/src/main/java/tutorly/model/filter/DateSessionFilter.java @@ -0,0 +1,43 @@ +package tutorly.model.filter; + +import java.time.LocalDate; +import java.util.function.Predicate; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.session.Session; + +/** + * Represents a filter for a {@code Session} whose {@code Date} matches the given date. + */ +public class DateSessionFilter implements Filter { + private final LocalDate date; + + public DateSessionFilter(LocalDate date) { + this.date = date; + } + + @Override + public Predicate toPredicate(ReadOnlyAddressBook addressBook) { + return session -> session.containsDate(date); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DateSessionFilter otherDateSessionFilter)) { + return false; + } + + return date.equals(otherDateSessionFilter.date); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("date", date).toString(); + } +} diff --git a/src/main/java/tutorly/model/filter/Filter.java b/src/main/java/tutorly/model/filter/Filter.java new file mode 100644 index 00000000000..a9c0fbab301 --- /dev/null +++ b/src/main/java/tutorly/model/filter/Filter.java @@ -0,0 +1,29 @@ +package tutorly.model.filter; + +import java.util.List; +import java.util.function.Predicate; + +import tutorly.model.ReadOnlyAddressBook; + +/** + * Represents a filter that can be used to filter a list of objects. + */ +@FunctionalInterface +public interface Filter { + + /** + * Returns a filter that represents the logical OR of the given filters. + */ + public static Filter any(List> filters) { + return new AnyFilter<>(filters); + } + + /** + * Returns the predicate that represents the filter. + * + * @param addressBook The address book context. + * @return A predicate that can be used for filtering. + */ + public Predicate toPredicate(ReadOnlyAddressBook addressBook); + +} diff --git a/src/main/java/tutorly/model/filter/NameContainsKeywordsFilter.java b/src/main/java/tutorly/model/filter/NameContainsKeywordsFilter.java new file mode 100644 index 00000000000..afcec65cf53 --- /dev/null +++ b/src/main/java/tutorly/model/filter/NameContainsKeywordsFilter.java @@ -0,0 +1,46 @@ +package tutorly.model.filter; + +import java.util.List; +import java.util.function.Predicate; + +import tutorly.commons.util.StringUtil; +import tutorly.commons.util.ToStringBuilder; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.person.Person; + +/** + * Represents a filter for a {@code Person} whose {@code Name} matches any of the keywords given. + */ +public class NameContainsKeywordsFilter implements Filter { + private final List keywords; + + public NameContainsKeywordsFilter(List keywords) { + this.keywords = keywords; + } + + @Override + public Predicate toPredicate(ReadOnlyAddressBook addressBook) { + return person -> keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof NameContainsKeywordsFilter otherNameContainsKeywordsFilter)) { + return false; + } + + return keywords.equals(otherNameContainsKeywordsFilter.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } + +} diff --git a/src/main/java/tutorly/model/filter/PhoneContainsKeywordsFilter.java b/src/main/java/tutorly/model/filter/PhoneContainsKeywordsFilter.java new file mode 100644 index 00000000000..54fad1314b4 --- /dev/null +++ b/src/main/java/tutorly/model/filter/PhoneContainsKeywordsFilter.java @@ -0,0 +1,46 @@ +package tutorly.model.filter; + +import java.util.List; +import java.util.function.Predicate; + +import tutorly.commons.util.StringUtil; +import tutorly.commons.util.ToStringBuilder; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.person.Person; + +/** + * Tests that a {@code Person}'s {@code Phone} matches any of the keywords given. + */ +public class PhoneContainsKeywordsFilter implements Filter { + private final List keywords; + + public PhoneContainsKeywordsFilter(List keywords) { + this.keywords = keywords; + } + + @Override + public Predicate toPredicate(ReadOnlyAddressBook addressBook) { + return person -> keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getPhone().value, keyword)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PhoneContainsKeywordsFilter otherNameContainsKeywordsFilter)) { + return false; + } + + return keywords.equals(otherNameContainsKeywordsFilter.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } + +} diff --git a/src/main/java/tutorly/model/filter/SubjectContainsKeywordsFilter.java b/src/main/java/tutorly/model/filter/SubjectContainsKeywordsFilter.java new file mode 100644 index 00000000000..e377ab9e380 --- /dev/null +++ b/src/main/java/tutorly/model/filter/SubjectContainsKeywordsFilter.java @@ -0,0 +1,45 @@ +package tutorly.model.filter; + +import java.util.List; +import java.util.function.Predicate; + +import tutorly.commons.util.StringUtil; +import tutorly.commons.util.ToStringBuilder; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.session.Session; + +/** + * Represents a filter for a {@code Session} whose {@code Subject} matches any of the keywords given. + */ +public class SubjectContainsKeywordsFilter implements Filter { + private final List keywords; + + public SubjectContainsKeywordsFilter(List keywords) { + this.keywords = keywords; + } + + @Override + public Predicate toPredicate(ReadOnlyAddressBook addressBook) { + return session -> keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(session.getSubject().subjectName, keyword)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SubjectContainsKeywordsFilter otherSubjectContainsKeywordsFilter)) { + return false; + } + + return keywords.equals(otherSubjectContainsKeywordsFilter.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/tutorly/model/person/Address.java similarity index 65% rename from src/main/java/seedu/address/model/person/Address.java rename to src/main/java/tutorly/model/person/Address.java index 469a2cc9a1e..672fb1b4c32 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/tutorly/model/person/Address.java @@ -1,7 +1,7 @@ -package seedu.address.model.person; +package tutorly.model.person; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static tutorly.commons.util.AppUtil.checkArgument; /** * Represents a Person's address in the address book. @@ -9,7 +9,10 @@ */ public class Address { - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; + public static final int MAX_LENGTH = 255; + + public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank. " + + "The maximum length is " + MAX_LENGTH + " characters."; /* * The first character of the address must not be a whitespace, @@ -17,8 +20,17 @@ public class Address { */ public static final String VALIDATION_REGEX = "[^\\s].*"; + private static final Address EMPTY_ADDRESS = new Address(); + public final String value; + /** + * Constructs an empty {@code Address} instance. + */ + private Address() { + value = ""; + } + /** * Constructs an {@code Address}. * @@ -30,11 +42,18 @@ public Address(String address) { value = address; } + /** + * Returns an empty Address instance. + */ + public static Address empty() { + return EMPTY_ADDRESS; + } + /** * Returns true if a given string is a valid email. */ public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); + return test.matches(VALIDATION_REGEX) && test.length() <= MAX_LENGTH; } @Override @@ -49,11 +68,10 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof Address)) { + if (!(other instanceof Address otherAddress)) { return false; } - Address otherAddress = (Address) other; return value.equals(otherAddress.value); } diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/tutorly/model/person/Email.java similarity index 74% rename from src/main/java/seedu/address/model/person/Email.java rename to src/main/java/tutorly/model/person/Email.java index c62e512bc29..6eb9ce245e6 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/tutorly/model/person/Email.java @@ -1,7 +1,7 @@ -package seedu.address.model.person; +package tutorly.model.person; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static tutorly.commons.util.AppUtil.checkArgument; /** * Represents a Person's email in the address book. @@ -9,8 +9,11 @@ */ public class Email { + public static final int MAX_LENGTH = 254; // Following erratum for RFC 3696 + private static final String SPECIAL_CHARACTERS = "+_.-"; - public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " + public static final String MESSAGE_CONSTRAINTS = "The maximum length is " + MAX_LENGTH + " characters. " + + "Emails should be of the format local-part@domain " + "and adhere to the following constraints:\n" + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " + "the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " @@ -27,12 +30,21 @@ public class Email { + ALPHANUMERIC_NO_UNDERSCORE + ")*"; private static final String DOMAIN_PART_REGEX = ALPHANUMERIC_NO_UNDERSCORE + "(-" + ALPHANUMERIC_NO_UNDERSCORE + ")*"; - private static final String DOMAIN_LAST_PART_REGEX = "(" + DOMAIN_PART_REGEX + "){2,}$"; // At least two chars + private static final String DOMAIN_LAST_PART_REGEX = "(?=.{2,}$)(" + DOMAIN_PART_REGEX + ")$"; // At least two chars private static final String DOMAIN_REGEX = "(" + DOMAIN_PART_REGEX + "\\.)*" + DOMAIN_LAST_PART_REGEX; public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" + DOMAIN_REGEX; + private static final Email EMPTY_EMAIL = new Email(); + public final String value; + /** + * Constructs an empty {@code Email} instance. + */ + private Email() { + value = ""; + } + /** * Constructs an {@code Email}. * @@ -44,11 +56,18 @@ public Email(String email) { value = email; } + /** + * Returns an empty Email instance. + */ + public static Email empty() { + return EMPTY_EMAIL; + } + /** * Returns if a given string is a valid email. */ public static boolean isValidEmail(String test) { - return test.matches(VALIDATION_REGEX); + return test.matches(VALIDATION_REGEX) && test.length() <= MAX_LENGTH; } @Override @@ -63,11 +82,10 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof Email)) { + if (!(other instanceof Email otherEmail)) { return false; } - Email otherEmail = (Email) other; return value.equals(otherEmail.value); } diff --git a/src/main/java/tutorly/model/person/Identity.java b/src/main/java/tutorly/model/person/Identity.java new file mode 100644 index 00000000000..75d9df43a77 --- /dev/null +++ b/src/main/java/tutorly/model/person/Identity.java @@ -0,0 +1,90 @@ +package tutorly.model.person; + +import java.util.Objects; + +import tutorly.commons.util.ToStringBuilder; + +/** + * Represents a student's identity. + * Since a student's ID and Name are unique in the address book, they can be used to identify a student. + */ +public class Identity { + + public static final String MESSAGE_INVALID_IDENTITY = + "STUDENT_IDENTIFIER provided is not a valid student ID or name."; + public static final String MESSAGE_INVALID_ID = "Student ID must be a positive integer."; + public static final int UNKNOWN_ID = Integer.MAX_VALUE; + + private int id; + private Name name; + + /** + * Creates an identity with the given name. + * + * @param name The name of the student. + */ + public Identity(Name name) { + this.name = name; + } + + /** + * Creates an identity with the given ID. + * + * @param id The ID of the student. + */ + public Identity(int id) { + if (id < 1) { + throw new IllegalArgumentException(MESSAGE_INVALID_ID); + } + + this.id = id; + } + + public int getId() { + return id; + } + + /** + * Checks if the student ID is provided in the identity. + * + * @return True if the ID is present in the identity, false otherwise. + */ + public boolean isIdPresent() { + return id != 0; + } + + public Name getName() { + return name; + } + + /** + * Checks if the student name is provided in the identity. + * + * @return True if the name is present in the identity, false otherwise. + */ + public boolean isNamePresent() { + return name != null; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Identity otherIdentity)) { + return false; + } + + return id == otherIdentity.id && Objects.equals(name, otherIdentity.name); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("id", id) + .add("name", name) + .toString(); + } +} diff --git a/src/main/java/tutorly/model/person/Memo.java b/src/main/java/tutorly/model/person/Memo.java new file mode 100644 index 00000000000..4b8bdd6a57f --- /dev/null +++ b/src/main/java/tutorly/model/person/Memo.java @@ -0,0 +1,83 @@ +package tutorly.model.person; + +import static java.util.Objects.requireNonNull; +import static tutorly.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's memo in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidMemo(String)} + */ +public class Memo { + + public static final int MAX_LENGTH = 255; + + public static final String MESSAGE_CONSTRAINTS = "Memo can take any values, and it should not be blank. " + + "The maximum length is " + MAX_LENGTH + " characters."; + + /* + * The first character of the memo must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. Newlines are allowed. + */ + public static final String VALIDATION_REGEX = "[^\\s](?s).*"; + + private static final Memo EMPTY_MEMO = new Memo(); + + public final String value; + + /** + * Constructs an empty {@code Memo} instance. + */ + private Memo() { + value = ""; + } + + /** + * Constructs an {@code Memo}. + * + * @param memo A valid memo. + */ + public Memo(String memo) { + requireNonNull(memo); + checkArgument(isValidMemo(memo), MESSAGE_CONSTRAINTS); + value = memo; + } + + /** + * Returns an empty Memo instance. + */ + public static Memo empty() { + return EMPTY_MEMO; + } + + /** + * Returns true if a given string is a valid memo. + */ + public static boolean isValidMemo(String test) { + return test.matches(VALIDATION_REGEX) && test.length() <= MAX_LENGTH; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Memo otherMemo)) { + return false; + } + + return value.equals(otherMemo.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/tutorly/model/person/Name.java similarity index 56% rename from src/main/java/seedu/address/model/person/Name.java rename to src/main/java/tutorly/model/person/Name.java index 173f15b9b00..f7c61d17504 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/tutorly/model/person/Name.java @@ -1,7 +1,7 @@ -package seedu.address.model.person; +package tutorly.model.person; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static tutorly.commons.util.AppUtil.checkArgument; /** * Represents a Person's name in the address book. @@ -9,14 +9,18 @@ */ public class Name { - public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + public static final int MAX_LENGTH = 255; + + public static final String MESSAGE_CONSTRAINTS = "Names should not be blank. It should start with a letter, " + + "and only contain letters, numbers, spaces, and these special characters: ()@*-+=:;'<>,?/. " + + "Multiple intermediate spaces will be collapsed to one. " + + "The maximum length is " + MAX_LENGTH + " characters."; /* - * The first character of the address must not be a whitespace, + * The first character of the name must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "[\\p{L}][\\p{L} .'-@/]*"; public final String fullName; @@ -35,7 +39,7 @@ public Name(String name) { * Returns true if a given string is a valid name. */ public static boolean isValidName(String test) { - return test.matches(VALIDATION_REGEX); + return test.matches(VALIDATION_REGEX) && test.length() <= MAX_LENGTH; } @@ -51,12 +55,11 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof Name)) { + if (!(other instanceof Name otherName)) { return false; } - Name otherName = (Name) other; - return fullName.equals(otherName.fullName); + return fullName.equalsIgnoreCase(otherName.fullName); } @Override diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/tutorly/model/person/Person.java similarity index 61% rename from src/main/java/seedu/address/model/person/Person.java rename to src/main/java/tutorly/model/person/Person.java index abe8c46b535..45a57faa739 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/tutorly/model/person/Person.java @@ -1,40 +1,68 @@ -package seedu.address.model.person; +package tutorly.model.person; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static tutorly.commons.util.CollectionUtil.requireAllNonNull; import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Set; -import seedu.address.commons.util.ToStringBuilder; -import seedu.address.model.tag.Tag; +import tutorly.commons.util.ToStringBuilder; +import tutorly.model.AddressBook; +import tutorly.model.tag.Tag; /** - * Represents a Person in the address book. + * Represents a student in the address book. * Guarantees: details are present and not null, field values are validated, immutable. + * Optional fields with empty string values are considered as not provided. */ public class Person { + public static final String MESSAGE_REASSIGNED_ID = "Student ID has already been set for this person."; + public static final String MESSAGE_INVALID_ID = "Student ID must be a positive integer."; + // Identity fields + private int id; // id field is effectively final private final Name name; - private final Phone phone; - private final Email email; // Data fields + private final Phone phone; + private final Email email; private final Address address; private final Set tags = new HashSet<>(); + private final Memo memo; /** * 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, Set tags, Memo memo) { + requireAllNonNull(name, phone, email, address, tags, memo); this.name = name; this.phone = phone; this.email = email; this.address = address; this.tags.addAll(tags); + this.memo = memo; + } + + /** + * Sets the student ID assigned by the address book during {@link AddressBook#addPerson(Person)}. Should only be + * called once per student as the student ID is effectively final. + */ + public void setId(int studentId) { + if (this.id != 0) { + throw new IllegalStateException(MESSAGE_REASSIGNED_ID); + } + + if (studentId < 1) { + throw new IllegalArgumentException(MESSAGE_INVALID_ID); + } + + this.id = studentId; + } + + public int getId() { + return id; } public Name getName() { @@ -61,6 +89,10 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + public Memo getMemo() { + return memo; + } + /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. @@ -85,32 +117,34 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof Person)) { + if (!(other instanceof Person otherPerson)) { return false; } - Person otherPerson = (Person) other; return name.equals(otherPerson.name) && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) - && tags.equals(otherPerson.tags); + && tags.equals(otherPerson.tags) + && memo.equals(otherPerson.memo); } @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(id, name, phone, email, address, tags, memo); } @Override public String toString() { return new ToStringBuilder(this) + .add("id", id) .add("name", name) .add("phone", phone) .add("email", email) .add("address", address) .add("tags", tags) + .add("memo", memo) .toString(); } diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/tutorly/model/person/Phone.java similarity index 51% rename from src/main/java/seedu/address/model/person/Phone.java rename to src/main/java/tutorly/model/person/Phone.java index d733f63d739..315043fd52b 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/tutorly/model/person/Phone.java @@ -1,7 +1,7 @@ -package seedu.address.model.person; +package tutorly.model.person; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static tutorly.commons.util.AppUtil.checkArgument; /** * Represents a Person's phone number in the address book. @@ -9,12 +9,26 @@ */ public class Phone { + public static final int MIN_LENGTH = 3; + public static final int MAX_LENGTH = 25; + + public static final String MESSAGE_CONSTRAINTS = "Phone numbers should not be blank. It should only contain " + + "numbers, spaces, hyphens, and an optional country code prefix. " + + "It should be between " + MIN_LENGTH + " to " + MAX_LENGTH + " characters long."; + + public static final String VALIDATION_REGEX = "(\\+\\d{1,3}( )?)?[\\d -]+"; + + private static final Phone EMPTY_PHONE = new Phone(); - public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String VALIDATION_REGEX = "\\d{3,}"; public final String value; + /** + * Constructs an empty {@code Phone} instance. + */ + private Phone() { + value = ""; + } + /** * Constructs a {@code Phone}. * @@ -26,11 +40,18 @@ public Phone(String phone) { value = phone; } + /** + * Returns an empty Phone instance. + */ + public static Phone empty() { + return EMPTY_PHONE; + } + /** * Returns true if a given string is a valid phone number. */ public static boolean isValidPhone(String test) { - return test.matches(VALIDATION_REGEX); + return test.matches(VALIDATION_REGEX) && test.length() >= MIN_LENGTH && test.length() <= MAX_LENGTH; } @Override @@ -45,11 +66,10 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof Phone)) { + if (!(other instanceof Phone otherPhone)) { return false; } - Phone otherPhone = (Phone) other; return value.equals(otherPhone.value); } diff --git a/src/main/java/tutorly/model/person/UniquePersonList.java b/src/main/java/tutorly/model/person/UniquePersonList.java new file mode 100644 index 00000000000..812f57e18dc --- /dev/null +++ b/src/main/java/tutorly/model/person/UniquePersonList.java @@ -0,0 +1,49 @@ +package tutorly.model.person; + +import java.util.Optional; + +import tutorly.model.uniquelist.UniqueList; + +/** + * A list of persons that enforces uniqueness between its elements and does not allow nulls. + * A person is considered unique by comparing using {@code Person#isSamePerson(Person)}. + * + * @see Person#isSamePerson(Person) + */ +public class UniquePersonList extends UniqueList { + + @Override + protected boolean isEquivalent(Person a, Person b) { + return a.isSamePerson(b); + } + + @Override + protected int compare(Person a, Person b) { + return Integer.compare(a.getId(), b.getId()); + } + + /** + * Returns the person with the given ID if it exists. + * + * @param id The ID of the person to retrieve. + * @return The person with the given ID. + */ + public Optional getPersonById(int id) { + return internalList.stream() + .filter(person -> person.getId() == id) + .findFirst(); + } + + /** + * Returns the person with the given name if it exists. + * + * @param name The name of the person to retrieve. + * @return The person with the given name. + */ + public Optional getPersonByName(Name name) { + return internalList.stream() + .filter(person -> person.getName().equals(name)) + .findFirst(); + } + +} diff --git a/src/main/java/tutorly/model/session/Session.java b/src/main/java/tutorly/model/session/Session.java new file mode 100644 index 00000000000..50f55291f23 --- /dev/null +++ b/src/main/java/tutorly/model/session/Session.java @@ -0,0 +1,128 @@ +package tutorly.model.session; + +import static tutorly.commons.util.CollectionUtil.requireAllNonNull; + +import java.time.LocalDate; + +import tutorly.commons.util.ToStringBuilder; +import tutorly.model.AddressBook; + +/** + * Represents a tutoring session for a student. + */ +public class Session { + + public static final String MESSAGE_REASSIGNED_ID = "Session ID has already been set for this session."; + public static final String MESSAGE_INVALID_ID = "Session ID must be a positive integer."; + public static final int UNKNOWN_ID = Integer.MAX_VALUE; + + private int id; // id field is effectively final + private final Timeslot timeslot; + private final Subject subject; + + /** + * Constructs a new Session. Every field must be present and not null. + * + * @param timeslot The start and end datetime of the session. + * @param subject The subject of the session. + */ + public Session(Timeslot timeslot, Subject subject) { + requireAllNonNull(timeslot, subject); + this.timeslot = timeslot; + this.subject = subject; + } + + /** + * Sets the session ID assigned by the address book during {@link AddressBook#addSession(Session)}. Should only be + * called once per session as the session ID is effectively final. + */ + public void setId(int id) { + if (this.id != 0) { + throw new IllegalStateException(MESSAGE_REASSIGNED_ID); + } + + if (id < 1) { + throw new IllegalArgumentException(MESSAGE_INVALID_ID); + } + + this.id = id; + } + + /** + * Checks if a date falls within this session + * Inclusive of start and end date. + */ + public boolean containsDate(LocalDate date) { + return timeslot.containsDate(date); + } + + public int getId() { + return id; + } + + public Timeslot getTimeslot() { + return timeslot; + } + + public Subject getSubject() { + return subject; + } + + /** + * Returns true if both sessions have the same ID. + * This defines a weaker notion of equality between two sessions. + */ + public boolean isSameSession(Session otherSession) { + if (otherSession == this) { + return true; + } + + return otherSession != null + && id == otherSession.id; + } + + /** + * Returns true if both sessions have overlapping timeslots. + */ + public boolean hasOverlappingTimeslot(Session otherSession) { + if (otherSession == this) { + return true; + } + + return otherSession != null + && timeslot.isOverlapping(otherSession.timeslot); + } + + /** + * Returns true if both sessions have the same identity and data fields. + * This defines a stronger notion of equality between two sessions. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Session otherSession)) { + return false; + } + + return id == otherSession.id + && timeslot.equals(otherSession.timeslot) + && subject.equals(otherSession.subject); + } + + /** + * Returns a string representation of the session. + * + * @return A formatted string with session ID, timeslot, and subject. + */ + @Override + public String toString() { + return new ToStringBuilder(this) + .add("id", id) + .add("timeslot", timeslot) + .add("subject", subject) + .toString(); + } +} diff --git a/src/main/java/tutorly/model/session/Subject.java b/src/main/java/tutorly/model/session/Subject.java new file mode 100644 index 00000000000..e637df35425 --- /dev/null +++ b/src/main/java/tutorly/model/session/Subject.java @@ -0,0 +1,66 @@ +package tutorly.model.session; + +import static java.util.Objects.requireNonNull; +import static tutorly.commons.util.AppUtil.checkArgument; + +/** + * Represents a Subject in the system. + * Guarantees: immutable; is valid as declared in {@link #isValidSubject(String)} + */ +public class Subject { + + public static final int MAX_LENGTH = 20; + + public static final String MESSAGE_CONSTRAINTS = "Subjects can take any values, and it should not be blank. " + + "The maximum length is " + MAX_LENGTH + " characters."; + + /* + * The first character of the subject must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "[^\\s](?s).*"; + + public final String subjectName; + + /** + * Constructs a {@code Subject}. + * + * @param subjectName A valid subject name. + */ + public Subject(String subjectName) { + requireNonNull(subjectName); + checkArgument(isValidSubject(subjectName), MESSAGE_CONSTRAINTS); + this.subjectName = subjectName; + } + + /** + * Returns true if a given string is a valid subject name. + */ + public static boolean isValidSubject(String test) { + return test.matches(VALIDATION_REGEX) && test.length() <= MAX_LENGTH; + } + + @Override + public String toString() { + return subjectName; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Subject otherSubject)) { + return false; + } + + return subjectName.equals(otherSubject.subjectName); + } + + @Override + public int hashCode() { + return subjectName.hashCode(); + } +} diff --git a/src/main/java/tutorly/model/session/Timeslot.java b/src/main/java/tutorly/model/session/Timeslot.java new file mode 100644 index 00000000000..bd2b6c4ed3b --- /dev/null +++ b/src/main/java/tutorly/model/session/Timeslot.java @@ -0,0 +1,84 @@ +package tutorly.model.session; + +import static tutorly.commons.util.CollectionUtil.requireAllNonNull; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import tutorly.commons.util.ToStringBuilder; + +/** + * Represents a Session timeslot. + */ +public class Timeslot { + + public static final String MESSAGE_END_BEFORE_START_DATETIME = "End datetime must be after start datetime."; + + private final LocalDateTime startTime; + private final LocalDateTime endTime; + + /** + * Constructs a Timeslot with the given start and end datetimes. End datetime must be after start datetime. + * + * @param startTime The start datetime of the timeslot. + * @param endTime The end datetime of the timeslot. + */ + public Timeslot(LocalDateTime startTime, LocalDateTime endTime) { + requireAllNonNull(startTime, endTime); + if (!endTime.isAfter(startTime)) { + throw new IllegalArgumentException(MESSAGE_END_BEFORE_START_DATETIME); + } + + this.startTime = startTime; + this.endTime = endTime; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public LocalDateTime getEndTime() { + return endTime; + } + + /** + * Checks if this timeslot overlaps with another timeslot. + * + * @param other The other timeslot to check against. + * @return True if the timeslots overlap, false otherwise. + */ + public boolean isOverlapping(Timeslot other) { + return startTime.isBefore(other.endTime) && endTime.isAfter(other.startTime); + } + + /** + * Checks if a date falls within this timeslot. + * Inclusive of start and end date. + */ + public boolean containsDate(LocalDate date) { + return !date.isBefore(startTime.toLocalDate()) && !date.isAfter(endTime.toLocalDate()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Timeslot otherTimeslot)) { + return false; + } + + return startTime.equals(otherTimeslot.startTime) && endTime.equals(otherTimeslot.endTime); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("startTime", startTime) + .add("endTime", endTime) + .toString(); + } + +} diff --git a/src/main/java/tutorly/model/session/UniqueSessionList.java b/src/main/java/tutorly/model/session/UniqueSessionList.java new file mode 100644 index 00000000000..e9d54c1a293 --- /dev/null +++ b/src/main/java/tutorly/model/session/UniqueSessionList.java @@ -0,0 +1,50 @@ +package tutorly.model.session; + +import java.util.Optional; + +import tutorly.model.uniquelist.UniqueList; + +/** + * A list of sessions that enforces uniqueness between its elements and does not allow nulls. + * A session is considered unique by comparing using {@code Session#isSameSession(Session)}. + * + * @see Session#isSameSession(Session) + */ +public class UniqueSessionList extends UniqueList { + + @Override + protected boolean isEquivalent(Session a, Session b) { + return a.isSameSession(b); + } + + @Override + protected int compare(Session a, Session b) { + return Integer.compare(a.getId(), b.getId()); + } + + /** + * Returns the session with the given ID if it exists. + * + * @param id The ID of the session to retrieve. + * @return The session with the given ID. + */ + public Optional getSessionById(int id) { + return internalList.stream() + .filter(session -> session.getId() == id) + .findFirst(); + } + + /** + * Returns true if the session {@code toCheck} overlaps with any existing sessions in the list. + * This is done by checking if the timeslot of the session overlaps with any other session's timeslot. + * Conflicts with the same session are ignored. + * + * @param toCheck The session to check for overlap. + * @return True if there is an overlapping session, false otherwise. + */ + public boolean hasOverlappingSession(Session toCheck) { + return internalList.stream() + .anyMatch(session -> !session.isSameSession(toCheck) + && session.hasOverlappingTimeslot(toCheck)); + } +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/tutorly/model/tag/Tag.java similarity index 58% rename from src/main/java/seedu/address/model/tag/Tag.java rename to src/main/java/tutorly/model/tag/Tag.java index f1a0d4e233b..3ffa7e98d20 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/tutorly/model/tag/Tag.java @@ -1,7 +1,7 @@ -package seedu.address.model.tag; +package tutorly.model.tag; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static tutorly.commons.util.AppUtil.checkArgument; /** * Represents a Tag in the address book. @@ -9,8 +9,17 @@ */ public class Tag { - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + public static final int MAX_LENGTH = 20; + + public static final String MESSAGE_CONSTRAINTS = "Tag names can take any values, and can be blank if only one tag " + + "is provided while editing student details. If multiple tags are provided, all tag names should not be " + + "blank. The maximum length of a tag name is " + MAX_LENGTH + " characters."; + + /* + * The first character of the tag must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "[^\\s](?s).*"; public final String tagName; @@ -29,7 +38,7 @@ public Tag(String tagName) { * Returns true if a given string is a valid tag name. */ public static boolean isValidTagName(String test) { - return test.matches(VALIDATION_REGEX); + return test.matches(VALIDATION_REGEX) && test.length() <= MAX_LENGTH; } @Override @@ -39,11 +48,10 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof Tag)) { + if (!(other instanceof Tag otherTag)) { return false; } - Tag otherTag = (Tag) other; return tagName.equals(otherTag.tagName); } diff --git a/src/main/java/tutorly/model/uniquelist/UniqueList.java b/src/main/java/tutorly/model/uniquelist/UniqueList.java new file mode 100644 index 00000000000..d0cf475f203 --- /dev/null +++ b/src/main/java/tutorly/model/uniquelist/UniqueList.java @@ -0,0 +1,198 @@ +package tutorly.model.uniquelist; + +import static java.util.Objects.requireNonNull; +import static tutorly.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import javafx.collections.ObservableList; +import tutorly.commons.util.ObservableListUtil; +import tutorly.model.uniquelist.exceptions.DuplicateElementException; +import tutorly.model.uniquelist.exceptions.ElementNotFoundException; + +/** + * A list that enforces uniqueness between its elements and does not allow nulls. + * An element is considered unique by comparing using {@code UniqueList#isEquivalent(T, T)}. As such, adding and + * updating of elements uses {@code UniqueList#isEquivalent(T, T)} for equivalence so as to ensure that the element + * being added or updated is unique in the UniqueList. However, the removal of an element uses {@code T#equals(Object)} + * so as to ensure that the exact element will be removed. + * Order can be enforced by implementing {@code UniqueList#compare(T, T)}. This guarantees that the list will always + * be sorted in the defined order. + *

+ * Supports a minimal set of list operations. + */ +public class UniqueList implements Iterable { + + protected final ObservableList internalList = ObservableListUtil.arrayList(); + protected final ObservableList internalUnmodifiableList = ObservableListUtil.unmodifiableList(internalList); + + /** + * Returns true if the list contains an equivalent element as the given argument. + */ + public boolean contains(T toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(element -> isEquivalent(element, toCheck)); + } + + /** + * Returns the equivalent element in the list. + */ + public Optional find(T toFind) { + requireNonNull(toFind); + return internalList.stream() + .filter(element -> isEquivalent(element, toFind)) + .findFirst(); + } + + /** + * Adds an element to the list. + * The element must not already exist in the list. + */ + public void add(T toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateElementException(); + } + + internalList.add(toAdd); + internalList.sort(this::compare); + } + + /** + * Replaces the element {@code target} in the list with {@code edited}. + * {@code target} must exist in the list. + * The edited element must not be equivalent to another existing element in the list. + */ + public void set(T target, T edited) { + requireAllNonNull(target, edited); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new ElementNotFoundException(); + } + + if (!isEquivalent(target, edited) && contains(edited)) { + throw new DuplicateElementException(); + } + + internalList.set(index, edited); + internalList.sort(this::compare); + } + + /** + * Removes the matching element from the list. + * The element must exist in the list. + */ + public void remove(T toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new ElementNotFoundException(); + } + } + + /** + * Replaces the contents of this list with {@code replacement}. + * {@code replacement} must not contain duplicate elements. + */ + public void setAll(UniqueList replacement) { + setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code replacement}. + * {@code replacement} must not contain duplicate elements. + */ + public void setAll(List replacement) { + requireAllNonNull(replacement); + if (!elementsAreUnique(replacement)) { + throw new DuplicateElementException(); + } + + internalList.setAll(replacement); + internalList.sort(this::compare); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + /** + * Returns the number of elements in the list. If the list contains more than {@code Integer.MAX_VALUE} elements, + * returns {@code Integer.MAX_VALUE}. + */ + public int size() { + return internalList.size(); + } + + /** + * Removes all elements from the list. + */ + public void clear() { + internalList.clear(); + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof UniqueList otherUniqueList)) { + return false; + } + + return internalList.equals(otherUniqueList.internalList); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + @Override + public String toString() { + return internalList.toString(); + } + + /** + * Returns true if the list contains only unique elements. + */ + private boolean elementsAreUnique(List list) { + for (int i = 0; i < list.size() - 1; i++) { + for (int j = i + 1; j < list.size(); j++) { + if (isEquivalent(list.get(i), list.get(j))) { + return false; + } + } + } + return true; + } + + /** + * Returns true if two elements are equivalent, and false otherwise. + */ + protected boolean isEquivalent(T element1, T element2) { + return element1.equals(element2); + } + + /** + * Compares two elements and returns an integer indicating their order. + * + * @return A negative integer, zero, or a positive integer as the first argument is less than, + * equal to, or greater than the second. + */ + protected int compare(T element1, T element2) { + return 0; + } +} diff --git a/src/main/java/tutorly/model/uniquelist/exceptions/DuplicateElementException.java b/src/main/java/tutorly/model/uniquelist/exceptions/DuplicateElementException.java new file mode 100644 index 00000000000..27e311f8e28 --- /dev/null +++ b/src/main/java/tutorly/model/uniquelist/exceptions/DuplicateElementException.java @@ -0,0 +1,10 @@ +package tutorly.model.uniquelist.exceptions; + +/** + * Signals that the operation will result in duplicate elements + */ +public class DuplicateElementException extends RuntimeException { + public DuplicateElementException() { + super("Operation would result in duplicate elements"); + } +} diff --git a/src/main/java/tutorly/model/uniquelist/exceptions/ElementNotFoundException.java b/src/main/java/tutorly/model/uniquelist/exceptions/ElementNotFoundException.java new file mode 100644 index 00000000000..fe1c181b6f2 --- /dev/null +++ b/src/main/java/tutorly/model/uniquelist/exceptions/ElementNotFoundException.java @@ -0,0 +1,6 @@ +package tutorly.model.uniquelist.exceptions; + +/** + * Signals that the operation is unable to find the specified element. + */ +public class ElementNotFoundException extends RuntimeException {} diff --git a/src/main/java/tutorly/model/util/SampleDataUtil.java b/src/main/java/tutorly/model/util/SampleDataUtil.java new file mode 100644 index 00000000000..7579504d497 --- /dev/null +++ b/src/main/java/tutorly/model/util/SampleDataUtil.java @@ -0,0 +1,102 @@ +package tutorly.model.util; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import tutorly.model.AddressBook; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.attendancerecord.Feedback; +import tutorly.model.person.Address; +import tutorly.model.person.Email; +import tutorly.model.person.Memo; +import tutorly.model.person.Name; +import tutorly.model.person.Person; +import tutorly.model.person.Phone; +import tutorly.model.session.Session; +import tutorly.model.session.Subject; +import tutorly.model.session.Timeslot; +import tutorly.model.tag.Tag; + +/** + * Contains utility methods for populating {@code AddressBook} with sample data. + */ +public class SampleDataUtil { + public static Person[] getSamplePersons() { + return new Person[] { + new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), + new Address("Blk 30 Geylang Street 29, #06-40"), + getTagSet("OLevels"), new Memo("Adept at calculus")), + 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("ALevels", "Priority"), new Memo("Struggles with memorising chemical compounds")), + 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("ALevels"), new Memo("Needs help with biology")), + 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("PSLE"), new Memo("Enjoys solving geometry problems")), + new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), + new Address("Blk 47 Tampines Street 20, #17-35"), + getTagSet("ALevels"), new Memo("Interested in electronics and circuits")), + new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), + new Address("Blk 45 Aljunied Street 85, #11-31"), + getTagSet("OLevels"), new Memo("Working on essay writing skills")) + }; + } + + public static Session[] getSampleSessions() { + return new Session[] { + new Session( + new Timeslot( + LocalDateTime.of(2025, 2, 20, 11, 30), + LocalDateTime.of(2025, 2, 20, 13, 30)), + new Subject("Math")), + new Session( + new Timeslot( + LocalDateTime.of(2025, 2, 21, 23, 0), + LocalDateTime.of(2025, 2, 22, 1, 0)), + new Subject("English")), + }; + } + + public static AttendanceRecord[] getSampleAttendanceRecords() { + return new AttendanceRecord[] { + new AttendanceRecord(1, 1, true, new Feedback("Good effort shown")), + new AttendanceRecord(3, 1, false, new Feedback("Absent without notice")), + new AttendanceRecord(5, 1, true, new Feedback("Did not complete homework")), + new AttendanceRecord(1, 2, false, Feedback.empty()), + new AttendanceRecord(2, 2, true, Feedback.empty()), + new AttendanceRecord(4, 2, false, Feedback.empty()), + }; + } + + public static ReadOnlyAddressBook getSampleAddressBook() { + AddressBook sampleAb = new AddressBook(); + for (Person samplePerson : getSamplePersons()) { + sampleAb.addPerson(samplePerson); + } + + for (Session sampleSession : getSampleSessions()) { + sampleAb.addSession(sampleSession); + } + + for (AttendanceRecord sampleRecord : getSampleAttendanceRecords()) { + sampleAb.addAttendanceRecord(sampleRecord); + } + + return sampleAb; + } + + /** + * Returns a tag set containing the list of strings given. + */ + public static Set getTagSet(String... strings) { + return Arrays.stream(strings) + .map(Tag::new) + .collect(Collectors.toSet()); + } + +} diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/tutorly/storage/AddressBookStorage.java similarity index 84% rename from src/main/java/seedu/address/storage/AddressBookStorage.java rename to src/main/java/tutorly/storage/AddressBookStorage.java index f2e015105ae..1119ec5721e 100644 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ b/src/main/java/tutorly/storage/AddressBookStorage.java @@ -1,14 +1,15 @@ -package seedu.address.storage; +package tutorly.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.model.ReadOnlyAddressBook; +import tutorly.commons.exceptions.DataLoadingException; +import tutorly.model.AddressBook; +import tutorly.model.ReadOnlyAddressBook; /** - * Represents a storage for {@link seedu.address.model.AddressBook}. + * Represents a storage for {@link AddressBook}. */ public interface AddressBookStorage { diff --git a/src/main/java/tutorly/storage/JsonAdaptedAttendanceRecord.java b/src/main/java/tutorly/storage/JsonAdaptedAttendanceRecord.java new file mode 100644 index 00000000000..07fa1809119 --- /dev/null +++ b/src/main/java/tutorly/storage/JsonAdaptedAttendanceRecord.java @@ -0,0 +1,66 @@ +package tutorly.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import tutorly.commons.exceptions.IllegalValueException; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.attendancerecord.Feedback; + +/** + * Jackson-friendly version of {@link AttendanceRecord}. + */ +public class JsonAdaptedAttendanceRecord { + + private final int studentId; + private final int sessionId; + private final boolean isPresent; + private final String feedback; + + /** + * Constructs a {@code JsonAdaptedAttendanceRecord} with the given attendance record details. + */ + @JsonCreator + public JsonAdaptedAttendanceRecord(@JsonProperty("studentId") int studentId, + @JsonProperty("sessionId") int sessionId, + @JsonProperty("isPresent") boolean isPresent, + @JsonProperty("feedback") String feedback) { + this.studentId = studentId; + this.sessionId = sessionId; + this.isPresent = isPresent; + this.feedback = feedback; + } + + /** + * Converts a given {@code AttendanceRecord} into this class for Jackson use. + */ + public JsonAdaptedAttendanceRecord(AttendanceRecord source) { + studentId = source.getStudentId(); + sessionId = source.getSessionId(); + isPresent = source.getAttendance(); + feedback = source.getFeedback().value; + } + + /** + * Converts this Jackson-friendly adapted attendace record object into the model's {@code AttendanceRecord} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted attendance record. + */ + public AttendanceRecord toModelType() throws IllegalValueException { + if (studentId <= 0 || sessionId <= 0) { + throw new IllegalValueException(AttendanceRecord.MESSAGE_CONSTRAINTS); + } + + final Feedback modelFeedback; + if (feedback == null || feedback.isEmpty()) { + modelFeedback = Feedback.empty(); + } else if (!Feedback.isValidFeedback(feedback)) { + throw new IllegalValueException(Feedback.MESSAGE_CONSTRAINTS); + } else { + modelFeedback = new Feedback(feedback); + } + + return new AttendanceRecord(studentId, sessionId, isPresent, modelFeedback); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/tutorly/storage/JsonAdaptedPerson.java similarity index 52% rename from src/main/java/seedu/address/storage/JsonAdaptedPerson.java rename to src/main/java/tutorly/storage/JsonAdaptedPerson.java index bd1ca0f56c8..75c98ad150d 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/tutorly/storage/JsonAdaptedPerson.java @@ -1,21 +1,21 @@ -package seedu.address.storage; +package tutorly.storage; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import tutorly.commons.exceptions.IllegalValueException; +import tutorly.model.person.Address; +import tutorly.model.person.Email; +import tutorly.model.person.Memo; +import tutorly.model.person.Name; +import tutorly.model.person.Person; +import tutorly.model.person.Phone; +import tutorly.model.tag.Tag; /** * Jackson-friendly version of {@link Person}. @@ -24,19 +24,23 @@ class JsonAdaptedPerson { public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; + private final int id; private final String name; private final String phone; private final String email; private final String address; private final List tags = new ArrayList<>(); + private final String memo; /** * Constructs a {@code JsonAdaptedPerson} with the given person details. */ @JsonCreator - public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + public JsonAdaptedPerson(@JsonProperty("id") int id, @JsonProperty("name") String name, + @JsonProperty("phone") String phone, @JsonProperty("email") String email, + @JsonProperty("address") String address, @JsonProperty("tags") List tags, + @JsonProperty("memo") String memo) { + this.id = id; this.name = name; this.phone = phone; this.email = email; @@ -44,19 +48,22 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone if (tags != null) { this.tags.addAll(tags); } + this.memo = memo; } /** * Converts a given {@code Person} into this class for Jackson use. */ public JsonAdaptedPerson(Person source) { + id = source.getId(); name = source.getName().fullName; phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) - .collect(Collectors.toList())); + .toList()); + memo = source.getMemo().value; } /** @@ -65,12 +72,17 @@ public JsonAdaptedPerson(Person source) { * @throws IllegalValueException if there were any data constraints violated in the adapted person. */ public Person toModelType() throws IllegalValueException { + if (id <= 0) { + throw new IllegalValueException(Person.MESSAGE_INVALID_ID); + } + final List personTags = new ArrayList<>(); for (JsonAdaptedTag tag : tags) { personTags.add(tag.toModelType()); } + final Set modelTags = new HashSet<>(personTags); - if (name == null) { + if (name == null || name.isEmpty()) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); } if (!Name.isValidName(name)) { @@ -78,32 +90,46 @@ public Person toModelType() throws IllegalValueException { } final Name modelName = new Name(name); - if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { + final Phone modelPhone; + if (phone == null || phone.isEmpty()) { + modelPhone = Phone.empty(); + } else if (!Phone.isValidPhone(phone)) { throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); + } else { + modelPhone = new Phone(phone); } - final Phone modelPhone = new Phone(phone); - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { + final Email modelEmail; + if (email == null || email.isEmpty()) { + modelEmail = Email.empty(); + } else if (!Email.isValidEmail(email)) { throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); + } else { + modelEmail = new Email(email); } - final Email modelEmail = new Email(email); - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { + final Address modelAddress; + if (address == null || address.isEmpty()) { + modelAddress = Address.empty(); + } else if (!Address.isValidAddress(address)) { throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); + } else { + modelAddress = new Address(address); } - final Address modelAddress = new Address(address); - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + final Memo modelMemo; + if (memo == null || memo.isEmpty()) { + modelMemo = Memo.empty(); + } else if (!Memo.isValidMemo(memo)) { + throw new IllegalValueException(Memo.MESSAGE_CONSTRAINTS); + } else { + modelMemo = new Memo(memo); + } + + Person person = new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags, modelMemo); + person.setId(id); + + return person; } } diff --git a/src/main/java/tutorly/storage/JsonAdaptedSession.java b/src/main/java/tutorly/storage/JsonAdaptedSession.java new file mode 100644 index 00000000000..92cea851357 --- /dev/null +++ b/src/main/java/tutorly/storage/JsonAdaptedSession.java @@ -0,0 +1,94 @@ +package tutorly.storage; + +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import tutorly.commons.exceptions.IllegalValueException; +import tutorly.logic.parser.ParserUtil; +import tutorly.model.session.Session; +import tutorly.model.session.Subject; +import tutorly.model.session.Timeslot; + +/** + * Jackson-friendly version of {@link Session}. + */ +class JsonAdaptedSession { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Session's %s field is missing!"; + + private final int id; + private final String startTime; + private final String endTime; + private final String subject; + + /** + * Constructs a {@code JsonAdaptedSession} with the given session details. + */ + @JsonCreator + public JsonAdaptedSession(@JsonProperty("id") int id, + @JsonProperty("startTime") String startTime, + @JsonProperty("endTime") String endTime, + @JsonProperty("subject") String subject) { + this.id = id; + this.startTime = startTime; + this.endTime = endTime; + this.subject = subject; + } + + /** + * Converts a given {@code Session} into this class for Jackson use. + */ + public JsonAdaptedSession(Session source) { + this.id = source.getId(); + this.subject = source.getSubject().subjectName; + + Timeslot timeslot = source.getTimeslot(); + this.startTime = timeslot.getStartTime().toString(); + this.endTime = timeslot.getEndTime().toString(); + } + + /** + * Converts this Jackson-friendly adapted session object into the model's {@code Session} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted session. + */ + public Session toModelType() throws IllegalValueException { + if (id <= 0) { + throw new IllegalValueException(Session.MESSAGE_INVALID_ID); + } + + if (startTime == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "startTime")); + } + + if (endTime == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "endTime")); + } + + final Timeslot modelTimeslot; + try { + modelTimeslot = new Timeslot(LocalDateTime.parse(startTime), LocalDateTime.parse(endTime)); + } catch (DateTimeParseException e) { + throw new IllegalValueException(ParserUtil.MESSAGE_INVALID_DATETIME); + } catch (IllegalArgumentException e) { + throw new IllegalValueException(e.getMessage()); + } + + if (subject == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "subject")); + } + if (!Subject.isValidSubject(subject)) { + throw new IllegalValueException(Subject.MESSAGE_CONSTRAINTS); + } + final Subject modelSubject = new Subject(subject); + + + Session session = new Session(modelTimeslot, modelSubject); + session.setId(id); + + return session; + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/tutorly/storage/JsonAdaptedTag.java similarity index 89% rename from src/main/java/seedu/address/storage/JsonAdaptedTag.java rename to src/main/java/tutorly/storage/JsonAdaptedTag.java index 0df22bdb754..891ae00b09e 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ b/src/main/java/tutorly/storage/JsonAdaptedTag.java @@ -1,10 +1,10 @@ -package seedu.address.storage; +package tutorly.storage; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; +import tutorly.commons.exceptions.IllegalValueException; +import tutorly.model.tag.Tag; /** * Jackson-friendly version of {@link Tag}. diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/tutorly/storage/JsonAddressBookStorage.java similarity index 86% rename from src/main/java/seedu/address/storage/JsonAddressBookStorage.java rename to src/main/java/tutorly/storage/JsonAddressBookStorage.java index 41e06f264e1..fac7380afeb 100644 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ b/src/main/java/tutorly/storage/JsonAddressBookStorage.java @@ -1,4 +1,4 @@ -package seedu.address.storage; +package tutorly.storage; import static java.util.Objects.requireNonNull; @@ -7,12 +7,12 @@ import java.util.Optional; import java.util.logging.Logger; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.commons.util.FileUtil; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyAddressBook; +import tutorly.commons.core.LogsCenter; +import tutorly.commons.exceptions.DataLoadingException; +import tutorly.commons.exceptions.IllegalValueException; +import tutorly.commons.util.FileUtil; +import tutorly.commons.util.JsonUtil; +import tutorly.model.ReadOnlyAddressBook; /** * A class to access AddressBook data stored as a json file on the hard disk. diff --git a/src/main/java/tutorly/storage/JsonSerializableAddressBook.java b/src/main/java/tutorly/storage/JsonSerializableAddressBook.java new file mode 100644 index 00000000000..472513ea6f4 --- /dev/null +++ b/src/main/java/tutorly/storage/JsonSerializableAddressBook.java @@ -0,0 +1,115 @@ +package tutorly.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import tutorly.commons.exceptions.IllegalValueException; +import tutorly.model.AddressBook; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.person.Person; +import tutorly.model.session.Session; + +/** + * An Immutable AddressBook that is serializable to JSON format. + */ +@JsonRootName(value = "addressbook") +class JsonSerializableAddressBook { + + public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_SESSION = "Sessions list contains duplicate session(s)."; + public static final String MESSAGE_DUPLICATE_ATTENDANCE_RECORD = + "Attendance records list contains duplicate attendance record(s)."; + public static final String MESSAGE_ILLEGAL_NEXT_PERSON_ID = "Next person ID is not valid."; + public static final String MESSAGE_ILLEGAL_NEXT_SESSION_ID = "Next session ID is not valid."; + + private final List persons = new ArrayList<>(); + private final List sessions = new ArrayList<>(); + private final List attendanceRecords = new ArrayList<>(); + + private final int nextPersonId; + private final int nextSessionId; + + /** + * Constructs a {@code JsonSerializableAddressBook} with the given persons, sessions, and attendance records. + */ + @JsonCreator + public JsonSerializableAddressBook(@JsonProperty("persons") List persons, + @JsonProperty("sessions") List sessions, + @JsonProperty("attendanceRecords") List attendanceRecords, + @JsonProperty("nextPersonId") int nextPersonId, @JsonProperty("nextSessionId") int nextSessionId) { + this.persons.addAll(persons); + this.sessions.addAll(sessions); + this.attendanceRecords.addAll(attendanceRecords); + this.nextPersonId = nextPersonId; + this.nextSessionId = nextSessionId; + } + + /** + * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableAddressBook}. + */ + public JsonSerializableAddressBook(ReadOnlyAddressBook source) { + persons.addAll(source.getPersonList().stream().map(JsonAdaptedPerson::new).collect(Collectors.toList())); + sessions.addAll(source.getSessionList().stream().map(JsonAdaptedSession::new).collect(Collectors.toList())); + attendanceRecords.addAll(source.getAttendanceRecordsList().stream() + .map(JsonAdaptedAttendanceRecord::new).collect(Collectors.toList())); + nextPersonId = source.getNextPersonId(); + nextSessionId = source.getNextSessionId(); + } + + /** + * Converts this address book into the model's {@code AddressBook} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public AddressBook toModelType() throws IllegalValueException { + AddressBook addressBook = new AddressBook(nextPersonId, nextSessionId); + + if (nextPersonId <= 0) { + throw new IllegalValueException(MESSAGE_ILLEGAL_NEXT_PERSON_ID); + } + + if (nextSessionId <= 0) { + throw new IllegalValueException(MESSAGE_ILLEGAL_NEXT_SESSION_ID); + } + + for (JsonAdaptedPerson jsonAdaptedPerson : persons) { + Person person = jsonAdaptedPerson.toModelType(); + if (addressBook.hasPerson(person)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); + } + if (person.getId() >= nextPersonId) { + throw new IllegalValueException(MESSAGE_ILLEGAL_NEXT_PERSON_ID); + } + addressBook.addPerson(person); + } + + for (JsonAdaptedSession jsonAdaptedSession : sessions) { + Session session = jsonAdaptedSession.toModelType(); + if (addressBook.hasSession(session)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_SESSION); + } + if (session.getId() >= nextSessionId) { + throw new IllegalValueException(MESSAGE_ILLEGAL_NEXT_SESSION_ID); + } + addressBook.addSession(session); + } + + for (JsonAdaptedAttendanceRecord jsonAdaptedAttendanceRecord : attendanceRecords) { + AttendanceRecord attendanceRecord = jsonAdaptedAttendanceRecord.toModelType(); + if (addressBook.hasAttendanceRecord(attendanceRecord)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_ATTENDANCE_RECORD); + } + addressBook.addAttendanceRecord(attendanceRecord); + } + + return addressBook; + } +} diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/tutorly/storage/JsonUserPrefsStorage.java similarity index 83% rename from src/main/java/seedu/address/storage/JsonUserPrefsStorage.java rename to src/main/java/tutorly/storage/JsonUserPrefsStorage.java index 48a9754807d..1ba898c0b9c 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/tutorly/storage/JsonUserPrefsStorage.java @@ -1,13 +1,13 @@ -package seedu.address.storage; +package tutorly.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import tutorly.commons.exceptions.DataLoadingException; +import tutorly.commons.util.JsonUtil; +import tutorly.model.ReadOnlyUserPrefs; +import tutorly.model.UserPrefs; /** * A class to access UserPrefs stored in the hard disk as a json file diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/tutorly/storage/Storage.java similarity index 73% rename from src/main/java/seedu/address/storage/Storage.java rename to src/main/java/tutorly/storage/Storage.java index 9fba0c7a1d6..c54f2066d6a 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/tutorly/storage/Storage.java @@ -1,13 +1,13 @@ -package seedu.address.storage; +package tutorly.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import tutorly.commons.exceptions.DataLoadingException; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.ReadOnlyUserPrefs; +import tutorly.model.UserPrefs; /** * API of the Storage component diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/tutorly/storage/StorageManager.java similarity index 89% rename from src/main/java/seedu/address/storage/StorageManager.java rename to src/main/java/tutorly/storage/StorageManager.java index 8b84a9024d5..e48256fb1a0 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/tutorly/storage/StorageManager.java @@ -1,15 +1,15 @@ -package seedu.address.storage; +package tutorly.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; import java.util.logging.Logger; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import tutorly.commons.core.LogsCenter; +import tutorly.commons.exceptions.DataLoadingException; +import tutorly.model.ReadOnlyAddressBook; +import tutorly.model.ReadOnlyUserPrefs; +import tutorly.model.UserPrefs; /** * Manages storage of AddressBook data in local storage. diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/tutorly/storage/UserPrefsStorage.java similarity index 69% rename from src/main/java/seedu/address/storage/UserPrefsStorage.java rename to src/main/java/tutorly/storage/UserPrefsStorage.java index e94ca422ea8..2957242c186 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/tutorly/storage/UserPrefsStorage.java @@ -1,15 +1,15 @@ -package seedu.address.storage; +package tutorly.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import tutorly.commons.exceptions.DataLoadingException; +import tutorly.model.ReadOnlyUserPrefs; +import tutorly.model.UserPrefs; /** - * Represents a storage for {@link seedu.address.model.UserPrefs}. + * Represents a storage for {@link UserPrefs}. */ public interface UserPrefsStorage { @@ -27,7 +27,7 @@ public interface UserPrefsStorage { Optional readUserPrefs() throws DataLoadingException; /** - * Saves the given {@link seedu.address.model.ReadOnlyUserPrefs} to the storage. + * Saves the given {@link tutorly.model.ReadOnlyUserPrefs} to the storage. * @param userPrefs cannot be null. * @throws IOException if there was any problem writing to the file. */ diff --git a/src/main/java/tutorly/ui/AttendanceRecordCard.java b/src/main/java/tutorly/ui/AttendanceRecordCard.java new file mode 100644 index 00000000000..e78369026f5 --- /dev/null +++ b/src/main/java/tutorly/ui/AttendanceRecordCard.java @@ -0,0 +1,59 @@ +package tutorly.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.util.Callback; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.person.Person; + +/** + * An UI component that displays information of a {@code AttendanceRecord}. + */ +public class AttendanceRecordCard extends UiPart { + + private static final String FXML = "AttendanceRecordListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final AttendanceRecord record; + + @FXML + private HBox cardPane; + @FXML + private CheckBox checkbox; + @FXML + private Label id; + @FXML + private Label name; + @FXML + private VBox container; + + /** + * Creates a {@code AttendanceRecordCard} with the given {@code AttendanceRecord} and index to display. + */ + public AttendanceRecordCard(AttendanceRecord record, Person student, boolean isSelected, + Callback toggleCallback) { + super(FXML); + this.record = record; + checkbox.setSelected(record.getAttendance()); + checkbox.setOnAction(event -> toggleCallback.call(!record.getAttendance())); + id.setText(student.getId() + ". "); + name.setText(student.getName().fullName); + name.setWrapText(isSelected); + + if (!record.getFeedback().value.isBlank()) { + container.getChildren().add( + new IconLabel(Icons.getMemoIcon(), record.getFeedback().value, isSelected).getRoot()); + } + } +} diff --git a/src/main/java/tutorly/ui/AttendanceRecordListPanel.java b/src/main/java/tutorly/ui/AttendanceRecordListPanel.java new file mode 100644 index 00000000000..c35a3c1281f --- /dev/null +++ b/src/main/java/tutorly/ui/AttendanceRecordListPanel.java @@ -0,0 +1,46 @@ +package tutorly.ui; + +import java.util.List; +import java.util.Optional; + +import javafx.collections.ObservableList; +import javafx.scene.layout.Region; +import javafx.util.Callback; +import tutorly.commons.util.ObservableListUtil; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.person.Person; +import tutorly.model.session.Session; + +/** + * Panel containing the list of attendance records. + */ +public class AttendanceRecordListPanel extends ListPanel { + + private final ObservableList students; + private final Callback toggleCallback; + + /** + * Creates a {@code AttendanceRecordListPanel} with the given records, students, and selected sessions. + */ + public AttendanceRecordListPanel(ObservableList records, ObservableList students, + ObservableList sessions, Callback toggleCallback) { + super(ObservableListUtil.filteredList(records, + record -> sessions.stream().anyMatch(session -> session.getId() == record.getSessionId()) + && students.stream().anyMatch(student -> student.getId() == record.getStudentId()), + List.of(students, sessions))); + + this.students = students; + this.toggleCallback = toggleCallback; + } + + @Override + protected UiPart getItemGraphic(AttendanceRecord record) { + Optional recordStudent = students.filtered(student -> student.getId() == record.getStudentId()) + .stream().findFirst(); + assert recordStudent.isPresent(); + + return new AttendanceRecordCard(record, recordStudent.get(), getSelected().contains(record), + newAttendance -> toggleCallback.call(record)); + }; + +} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/tutorly/ui/CommandBox.java similarity index 89% rename from src/main/java/seedu/address/ui/CommandBox.java rename to src/main/java/tutorly/ui/CommandBox.java index 9e75478664b..c5da89c6380 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/tutorly/ui/CommandBox.java @@ -1,12 +1,13 @@ -package seedu.address.ui; +package tutorly.ui; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; import javafx.scene.layout.Region; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; +import tutorly.logic.Logic; +import tutorly.logic.commands.CommandResult; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.logic.parser.exceptions.ParseException; /** * The UI component that is responsible for receiving user command inputs. @@ -77,7 +78,7 @@ public interface CommandExecutor { /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see Logic#execute(String) */ CommandResult execute(String commandText) throws CommandException, ParseException; } diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/tutorly/ui/HelpWindow.java similarity index 50% rename from src/main/java/seedu/address/ui/HelpWindow.java rename to src/main/java/tutorly/ui/HelpWindow.java index 3f16b2fcf26..fd6a1772089 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/tutorly/ui/HelpWindow.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package tutorly.ui; import java.util.logging.Logger; @@ -8,19 +8,55 @@ import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.stage.Stage; -import seedu.address.commons.core.LogsCenter; +import tutorly.commons.core.LogsCenter; /** * Controller for a help page */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; - public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; + public static final String USERGUIDE_URL = "https://ay2425s2-cs2103t-t17-3.github.io/tp/UserGuide.html"; + public static final String HELP_MESSAGE = "For more info, refer to the user guide: " + USERGUIDE_URL; + public static final String COMMAND_SUMMARY = """ +Command Summary: + - General commands: + - help: Shows this help message. + - clear: Clears all students and sessions from the app. + - exit: Closes the app. + - undo: Undoes the last successfully executed command. + + - Viewing tabs: + - student: Switches to the students tab. + - session: Switches to the sessions tab. + + - Student management: + - student add n/NAME [p/PHONE] [e/EMAIL] [a/ADDRESS] [m/MEMO] [t/TAG]…​: Adds a student. + - student list: Lists all students. + - student view STUDENT_IDENTIFIER: Scrolls to the details of the specified student. + - student edit STUDENT_IDENTIFIER [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [m/MEMO] [t/TAG]…​ : \ +Edits a student's details. + - student search [ses/SESSION_ID] [n/NAME_KEYWORDS] [p/PHONE_KEYWORDS]: Searches for students. + - student delete STUDENT_IDENTIFIER: Deletes a student. + + - Session management: + - session add t/TIMESLOT sub/SUBJECT: Adds a session. + - session list: Lists all sessions. + - session view SESSION_ID: Shows the attendance for the specified session. + - session edit SESSION_ID [t/TIMESLOT] [sub/SUBJECT]: Edits a session's details. + - session search [d/DATE] [sub/SUBJECT_KEYWORDS]: Searches for sessions. + - session delete SESSION_ID: Deletes a session. + - session enrol STUDENT_IDENTIFIER ses/SESSION_ID: Enrols a student to a session. + - session unenrol STUDENT_IDENTIFIER ses/SESSION_ID: Unenrols a student from a session. + - session mark STUDENT_IDENTIFIER ses/SESSION_ID: Marks attendance for a student in a session. + - session unmark STUDENT_IDENTIFIER ses/SESSION_ID: Unmarks attendance for a student in a session. + - session feedback STUDENT_IDENTIFIER ses/SESSION_ID f/FEEDBACK: \ +Adds or updates feedback for a student in a session."""; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); private static final String FXML = "HelpWindow.fxml"; + @FXML + private Label commandSummary; @FXML private Button copyButton; @@ -34,6 +70,7 @@ public class HelpWindow extends UiPart { */ public HelpWindow(Stage root) { super(FXML, root); + commandSummary.setText(COMMAND_SUMMARY); helpMessage.setText(HELP_MESSAGE); } diff --git a/src/main/java/tutorly/ui/IconLabel.java b/src/main/java/tutorly/ui/IconLabel.java new file mode 100644 index 00000000000..0d069071fe1 --- /dev/null +++ b/src/main/java/tutorly/ui/IconLabel.java @@ -0,0 +1,32 @@ +package tutorly.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; + +/** + * A UI component that displays an icon and a label. + */ +public class IconLabel extends UiPart { + + private static final String FXML = "IconLabel.fxml"; + + @FXML + private ImageView imageView; + @FXML + private Label label; + + /** + * Creates an {@code IconLabel} with the given {@code iconPath} and {@code text}. + */ + public IconLabel(Image image, String text, boolean shouldWrap) { + super(FXML); + + imageView.setImage(image); + label.setText(text); + label.setWrapText(shouldWrap); + } + +} diff --git a/src/main/java/tutorly/ui/Icons.java b/src/main/java/tutorly/ui/Icons.java new file mode 100644 index 00000000000..24fc5c10f99 --- /dev/null +++ b/src/main/java/tutorly/ui/Icons.java @@ -0,0 +1,44 @@ +package tutorly.ui; + +import javafx.scene.image.Image; + +/** + * Utility class for getting icons displayed. + */ +public class Icons { + + private static final String TELEPHONE_PATH = "/images/telephone.png"; + private static final String HOUSE_PATH = "/images/house.png"; + private static final String EMAIL_PATH = "/images/email.png"; + private static final String MEMO_PATH = "/images/memo.png"; + private static final String CALENDAR_PATH = "/images/calendar.png"; + + /** + * Returns the Image of the icon at the specified path. + * + * @param iconPath The path to the icon image. + */ + public static Image getIcon(String iconPath) { + return new Image(iconPath); + } + + public static Image getTelephoneIcon() { + return getIcon(TELEPHONE_PATH); + } + + public static Image getHouseIcon() { + return getIcon(HOUSE_PATH); + } + + public static Image getEmailIcon() { + return getIcon(EMAIL_PATH); + } + + public static Image getMemoIcon() { + return getIcon(MEMO_PATH); + } + + public static Image getCalendarIcon() { + return getIcon(CALENDAR_PATH); + } +} diff --git a/src/main/java/tutorly/ui/ListPanel.java b/src/main/java/tutorly/ui/ListPanel.java new file mode 100644 index 00000000000..ea3aa58d729 --- /dev/null +++ b/src/main/java/tutorly/ui/ListPanel.java @@ -0,0 +1,71 @@ +package tutorly.ui; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Region; + +/** + * Panel containing the list of items. + */ +public abstract class ListPanel extends UiPart { + private static final String FXML = "ListPanel.fxml"; + + @FXML + private ListView listView; + + /** + * Creates a {@code ListPanel} with the given {@code ObservableList}. + */ + public ListPanel(ObservableList observableList) { + super(FXML); + listView.setItems(observableList); + listView.setCellFactory(listView -> new ListViewCell()); + listView.addEventHandler(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode() == KeyCode.ESCAPE) { + listView.getSelectionModel().clearSelection(); + } + }); + } + + /** + * Returns an unmodifiable view of the items selected. + */ + public ObservableList getSelected() { + return listView.getSelectionModel().getSelectedItems(); + } + + /** + * Selects the given item. + */ + public void select(T item) { + listView.scrollTo(item); + listView.getSelectionModel().select(item); + } + + /** + * Returns the graphic for the given item. + */ + protected abstract UiPart getItemGraphic(T item); + + /** + * Custom {@code ListCell} that displays the graphics of the item. + */ + class ListViewCell extends ListCell { + @Override + protected void updateItem(T item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(getItemGraphic(item).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/tutorly/ui/MainWindow.java similarity index 67% rename from src/main/java/seedu/address/ui/MainWindow.java rename to src/main/java/tutorly/ui/MainWindow.java index 79e74ef37c0..a2c4601c071 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/tutorly/ui/MainWindow.java @@ -1,21 +1,29 @@ -package seedu.address.ui; +package tutorly.ui; + +import static java.util.Objects.requireNonNull; import java.util.logging.Logger; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.MenuItem; +import javafx.scene.control.TabPane; import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; import javafx.stage.Stage; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.Logic; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; +import tutorly.commons.core.GuiSettings; +import tutorly.commons.core.LogsCenter; +import tutorly.logic.Logic; +import tutorly.logic.commands.AttendanceMarkSessionCommand; +import tutorly.logic.commands.AttendanceUnmarkSessionCommand; +import tutorly.logic.commands.Command; +import tutorly.logic.commands.CommandResult; +import tutorly.logic.commands.exceptions.CommandException; +import tutorly.logic.parser.exceptions.ParseException; +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.person.Identity; /** * The Main Window. Provides the basic application layout containing @@ -32,9 +40,14 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private SessionListPanel sessionListPanel; + private AttendanceRecordListPanel attendanceRecordListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + @FXML + private TabPane tabPane; + @FXML private StackPane commandBoxPlaceholder; @@ -44,6 +57,12 @@ public class MainWindow extends UiPart { @FXML private StackPane personListPanelPlaceholder; + @FXML + private StackPane sessionListPanelPlaceholder; + + @FXML + private StackPane attendanceRecordListPanelPlaceholder; + @FXML private StackPane resultDisplayPlaceholder; @@ -113,6 +132,13 @@ void fillInnerParts() { personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + sessionListPanel = new SessionListPanel(logic.getFilteredSessionList()); + sessionListPanelPlaceholder.getChildren().add(sessionListPanel.getRoot()); + + attendanceRecordListPanel = new AttendanceRecordListPanel(logic.getAttendanceRecordList(), + logic.getPersonList(), sessionListPanel.getSelected(), this::toggleAttendanceRecord); + attendanceRecordListPanelPlaceholder.getChildren().add(attendanceRecordListPanel.getRoot()); + resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); @@ -163,14 +189,10 @@ private void handleExit() { primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; - } - /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see Logic#execute(String) */ private CommandResult executeCommand(String commandText) throws CommandException, ParseException { try { @@ -178,11 +200,19 @@ private CommandResult executeCommand(String commandText) throws CommandException logger.info("Result: " + commandResult.getFeedbackToUser()); resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); - if (commandResult.isShowHelp()) { + if (commandResult.shouldSwitchTab()) { + tabPane.getSelectionModel().select(commandResult.getTab().getTabId()); + + commandResult.getTab().getTargetPerson().ifPresent(person -> personListPanel.select(person)); + commandResult.getTab().getTargetSession().ifPresent(session -> sessionListPanel.select(session)); + commandResult.getTab().getTargetRecord().ifPresent(record -> attendanceRecordListPanel.select(record)); + } + + if (commandResult.shouldShowHelp()) { handleHelp(); } - if (commandResult.isExit()) { + if (commandResult.shouldExit()) { handleExit(); } @@ -193,4 +223,24 @@ private CommandResult executeCommand(String commandText) throws CommandException throw e; } } + + /** + * Toggles the attendance record of a student for a session. + */ + private Void toggleAttendanceRecord(AttendanceRecord record) { + requireNonNull(record); + + Command command = record.getAttendance() + ? new AttendanceUnmarkSessionCommand(new Identity(record.getStudentId()), record.getSessionId()) + : new AttendanceMarkSessionCommand(new Identity(record.getStudentId()), record.getSessionId()); + + try { + CommandResult commandResult = logic.execute(command); + resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + } catch (CommandException e) { + resultDisplay.setFeedbackToUser(e.getMessage()); + } + + return null; + } } diff --git a/src/main/java/tutorly/ui/PersonCard.java b/src/main/java/tutorly/ui/PersonCard.java new file mode 100644 index 00000000000..50a14138914 --- /dev/null +++ b/src/main/java/tutorly/ui/PersonCard.java @@ -0,0 +1,79 @@ +package tutorly.ui; + +import java.util.Comparator; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import tutorly.model.person.Address; +import tutorly.model.person.Email; +import tutorly.model.person.Memo; +import tutorly.model.person.Person; +import tutorly.model.person.Phone; + +/** + * An UI component that displays information of a {@code Person}. + */ +public class PersonCard extends UiPart { + + private static final String FXML = "PersonListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Person person; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label name; + @FXML + private FlowPane tags; + @FXML + private VBox container; + + /** + * Creates a {@code PersonCard} with the given {@code Person}. + */ + public PersonCard(Person person, boolean isSelected) { + super(FXML); + this.person = person; + id.setText(person.getId() + ". "); + name.setText(person.getName().fullName); + name.setWrapText(isSelected); + + person.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + + if (person.getPhone() != Phone.empty()) { + container.getChildren().add( + new IconLabel(Icons.getTelephoneIcon(), person.getPhone().value, isSelected).getRoot()); + } + + if (person.getAddress() != Address.empty()) { + container.getChildren().add( + new IconLabel(Icons.getHouseIcon(), person.getAddress().value, isSelected).getRoot()); + } + + if (person.getEmail() != Email.empty()) { + container.getChildren().add( + new IconLabel(Icons.getEmailIcon(), person.getEmail().value, isSelected).getRoot()); + } + + if (person.getMemo() != Memo.empty()) { + container.getChildren().add( + new IconLabel(Icons.getMemoIcon(), person.getMemo().value, isSelected).getRoot()); + } + } +} diff --git a/src/main/java/tutorly/ui/PersonListPanel.java b/src/main/java/tutorly/ui/PersonListPanel.java new file mode 100644 index 00000000000..7e8a8b2eb9d --- /dev/null +++ b/src/main/java/tutorly/ui/PersonListPanel.java @@ -0,0 +1,21 @@ +package tutorly.ui; + +import javafx.collections.ObservableList; +import javafx.scene.layout.Region; +import tutorly.model.person.Person; + +/** + * Panel containing the list of persons. + */ +public class PersonListPanel extends ListPanel { + + public PersonListPanel(ObservableList personList) { + super(personList); + } + + @Override + protected UiPart getItemGraphic(Person person) { + return new PersonCard(person, getSelected().contains(person)); + }; + +} diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/tutorly/ui/ResultDisplay.java similarity index 95% rename from src/main/java/seedu/address/ui/ResultDisplay.java rename to src/main/java/tutorly/ui/ResultDisplay.java index 7d98e84eedf..af48f526f8b 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/tutorly/ui/ResultDisplay.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package tutorly.ui; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/tutorly/ui/SessionCard.java b/src/main/java/tutorly/ui/SessionCard.java new file mode 100644 index 00000000000..0e8463ce7cb --- /dev/null +++ b/src/main/java/tutorly/ui/SessionCard.java @@ -0,0 +1,50 @@ +package tutorly.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import tutorly.logic.Messages; +import tutorly.model.session.Session; + +/** + * An UI component that displays information of a {@code Session}. + */ +public class SessionCard extends UiPart { + + private static final String FXML = "SessionListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Session session; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label subject; + @FXML + private VBox container; + + /** + * Creates a {@code SessionCard} with the given {@code Session}. + */ + public SessionCard(Session session, boolean isSelected) { + super(FXML); + this.session = session; + id.setText(session.getId() + ". "); + subject.setText(session.getSubject().subjectName); + subject.setWrapText(isSelected); + + container.getChildren().add( + new IconLabel(Icons.getCalendarIcon(), Messages.format(session.getTimeslot()), isSelected).getRoot()); + } +} diff --git a/src/main/java/tutorly/ui/SessionListPanel.java b/src/main/java/tutorly/ui/SessionListPanel.java new file mode 100644 index 00000000000..dc25eacf55f --- /dev/null +++ b/src/main/java/tutorly/ui/SessionListPanel.java @@ -0,0 +1,21 @@ +package tutorly.ui; + +import javafx.collections.ObservableList; +import javafx.scene.layout.Region; +import tutorly.model.session.Session; + +/** + * Panel containing the list of sessions. + */ +public class SessionListPanel extends ListPanel { + + public SessionListPanel(ObservableList sessionList) { + super(sessionList); + } + + @Override + protected UiPart getItemGraphic(Session session) { + return new SessionCard(session, getSelected().contains(session)); + }; + +} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/tutorly/ui/StatusBarFooter.java similarity index 96% rename from src/main/java/seedu/address/ui/StatusBarFooter.java rename to src/main/java/tutorly/ui/StatusBarFooter.java index b577f829423..d028c27c2a7 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/tutorly/ui/StatusBarFooter.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package tutorly.ui; import java.nio.file.Path; import java.nio.file.Paths; diff --git a/src/main/java/tutorly/ui/Tab.java b/src/main/java/tutorly/ui/Tab.java new file mode 100644 index 00000000000..cc4041379c2 --- /dev/null +++ b/src/main/java/tutorly/ui/Tab.java @@ -0,0 +1,83 @@ +package tutorly.ui; + +import java.util.Optional; + +import tutorly.model.attendancerecord.AttendanceRecord; +import tutorly.model.person.Person; +import tutorly.model.session.Session; + +/** + * Represents the tabs. + */ +public class Tab { + + public static final int TAB_ID_STUDENT = 0; + public static final int TAB_ID_SESSION = 1; + + private final int tabId; + private final Optional targetPerson; + private final Optional targetSession; + private final Optional targetRecord; + + private Tab(int tabId, Person targetPerson, Session targetSession, AttendanceRecord targetRecord) { + this.tabId = tabId; + this.targetPerson = Optional.ofNullable(targetPerson); + this.targetSession = Optional.ofNullable(targetSession); + this.targetRecord = Optional.ofNullable(targetRecord); + } + + public static Tab student() { + return new Tab(TAB_ID_STUDENT, null, null, null); + } + + public static Tab student(Person target) { + return new Tab(TAB_ID_STUDENT, target, null, null); + } + + public static Tab session() { + return new Tab(TAB_ID_SESSION, null, null, null); + } + + public static Tab session(Session target) { + return new Tab(TAB_ID_SESSION, null, target, null); + } + + public static Tab attendanceRecord(Session session, AttendanceRecord target) { + return new Tab(TAB_ID_SESSION, null, session, target); + } + + public int getTabId() { + return tabId; + } + + public Optional getTargetPerson() { + return targetPerson; + } + + public Optional getTargetSession() { + return targetSession; + } + + public Optional getTargetRecord() { + return targetRecord; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Tab)) { + return false; + } + + Tab otherTab = (Tab) other; + return tabId == otherTab.tabId + && targetPerson.equals(otherTab.targetPerson) + && targetSession.equals(otherTab.targetSession) + && targetRecord.equals(otherTab.targetRecord); + } + +} + diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/tutorly/ui/Ui.java similarity index 86% rename from src/main/java/seedu/address/ui/Ui.java rename to src/main/java/tutorly/ui/Ui.java index 17aa0b494fe..c01daa813d3 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/tutorly/ui/Ui.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package tutorly.ui; import javafx.stage.Stage; diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/tutorly/ui/UiManager.java similarity index 94% rename from src/main/java/seedu/address/ui/UiManager.java rename to src/main/java/tutorly/ui/UiManager.java index fdf024138bc..d0decac9ec7 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/tutorly/ui/UiManager.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package tutorly.ui; import java.util.logging.Logger; @@ -7,10 +7,10 @@ import javafx.scene.control.Alert.AlertType; import javafx.scene.image.Image; import javafx.stage.Stage; -import seedu.address.MainApp; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; +import tutorly.MainApp; +import tutorly.commons.core.LogsCenter; +import tutorly.commons.util.StringUtil; +import tutorly.logic.Logic; /** * The manager of the UI component. diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/tutorly/ui/UiPart.java similarity index 97% rename from src/main/java/seedu/address/ui/UiPart.java rename to src/main/java/tutorly/ui/UiPart.java index fc820e01a9c..c5b803a477c 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/tutorly/ui/UiPart.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package tutorly.ui; import static java.util.Objects.requireNonNull; @@ -6,7 +6,7 @@ import java.net.URL; import javafx.fxml.FXMLLoader; -import seedu.address.MainApp; +import tutorly.MainApp; /** * Represents a distinct part of the UI. e.g. Windows, dialogs, panels, status bars, etc. diff --git a/src/main/resources/images/calendar.png b/src/main/resources/images/calendar.png index 8b2bdf4f1c1..deb118f5dd0 100644 Binary files a/src/main/resources/images/calendar.png and b/src/main/resources/images/calendar.png differ diff --git a/src/main/resources/images/clock.png b/src/main/resources/images/clock.png deleted file mode 100644 index 0807cbf6451..00000000000 Binary files a/src/main/resources/images/clock.png and /dev/null differ diff --git a/src/main/resources/images/email.png b/src/main/resources/images/email.png new file mode 100644 index 00000000000..5e437e8a4ef Binary files /dev/null and b/src/main/resources/images/email.png differ diff --git a/src/main/resources/images/fail.png b/src/main/resources/images/fail.png deleted file mode 100644 index 6daf01290dd..00000000000 Binary files a/src/main/resources/images/fail.png and /dev/null differ diff --git a/src/main/resources/images/house.png b/src/main/resources/images/house.png new file mode 100644 index 00000000000..f45ff01c551 Binary files /dev/null and b/src/main/resources/images/house.png differ diff --git a/src/main/resources/images/info_icon.png b/src/main/resources/images/info_icon.png deleted file mode 100644 index f8cef714095..00000000000 Binary files a/src/main/resources/images/info_icon.png and /dev/null differ diff --git a/src/main/resources/images/memo.png b/src/main/resources/images/memo.png new file mode 100644 index 00000000000..7a88d334f57 Binary files /dev/null and b/src/main/resources/images/memo.png differ diff --git a/src/main/resources/images/telephone.png b/src/main/resources/images/telephone.png new file mode 100644 index 00000000000..23541e3a607 Binary files /dev/null and b/src/main/resources/images/telephone.png differ diff --git a/src/main/resources/view/AttendanceRecordListCard.fxml b/src/main/resources/view/AttendanceRecordListCard.fxml new file mode 100644 index 00000000000..46678d35c38 --- /dev/null +++ b/src/main/resources/view/AttendanceRecordListCard.fxml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..23eedd403b7 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -39,6 +39,26 @@ -fx-max-height: 0; } +.tab-pane .tab-header-background { + -fx-background-color: #383838; +} + +.tab-pane .tab { + -fx-background-color: derive(#1d1d1d, 70%);; +} + +.tab-pane .tab .tab-label { + -fx-text-fill: #d8d8d8; +} + +.tab-pane .tab:selected { + -fx-background-color: #1d1d1d; +} + +.tab-pane .tab:selected .tab-label { + -fx-text-fill: white; +} + .table-view { -fx-base: #1d1d1d; -fx-control-inner-background: #1d1d1d; diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css index 17e8a8722cd..4580304244a 100644 --- a/src/main/resources/view/HelpWindow.css +++ b/src/main/resources/view/HelpWindow.css @@ -1,4 +1,4 @@ -#copyButton, #helpMessage { +#copyButton, #helpMessage, #commandSummary { -fx-text-fill: white; } diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index e01f330de33..e8ede647e13 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -7,6 +7,7 @@ + @@ -18,27 +19,33 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/IconLabel.fxml b/src/main/resources/view/IconLabel.fxml new file mode 100644 index 00000000000..0f969609b04 --- /dev/null +++ b/src/main/resources/view/IconLabel.fxml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ListPanel.fxml b/src/main/resources/view/ListPanel.fxml new file mode 100644 index 00000000000..b1b5ebe1f5c --- /dev/null +++ b/src/main/resources/view/ListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..fd5e6776b48 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -7,12 +7,15 @@ + + + + - + @@ -33,24 +36,57 @@ - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index 84e09833a87..4e4f35ef2d5 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -7,6 +7,7 @@ + @@ -14,11 +15,11 @@ - + - + - - diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 01b691792a9..78041903698 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -3,7 +3,6 @@ - -