diff --git a/.github/workflows/gradletest.yml b/.github/workflows/gradletest.yml new file mode 100644 index 00000000000..3f454a0be30 --- /dev/null +++ b/.github/workflows/gradletest.yml @@ -0,0 +1,46 @@ +name: Java CI + +on: [push, pull_request] + +jobs: + build: + strategy: + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + + steps: + - name: Set up repository + uses: actions/checkout@main + + - name: Set up repository + uses: actions/checkout@main + with: + ref: master + + - name: Merge to master + run: git checkout --progress --force ${{ github.sha }} + + - name: Run repository-wide tests + if: runner.os == 'Linux' + working-directory: ${{ github.workspace }}/.github + run: ./run-checks.sh + + - name: Validate Gradle Wrapper + uses: gradle/actions/wrapper-validation@v3 + + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + java-package: jdk+fx + + - name: Build and check with Gradle + run: ./gradlew check coverage + + - name: Upload coverage reports to Codecov + if: runner.os == 'Linux' + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index 16208adb9b6..57f43b773e1 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![CI Status](https://github.com/AY2425S1-CS2103T-F13-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2425S1-CS2103T-F13-1/tp/actions) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org/#contributing-to-se-edu) for more info. +Welcome to **TAHub**. +* TAHub is a central platform designed specifically for teaching assistants (TAs) to organize, manage, and streamline student information, with plans to manage students' tasks. +* TAHub simplifies the role of Teaching Assistants (TAs) by providing a centralized hub to organize student information, and efficiently manage course-related tasks. This platform empowers TAs to focus more on enhancing student learning and less on administrative chaos. +* Why use TAHub? + * TAs supporting professors and lecturers in managing course-related tasks. + * New and experienced TAs handling multiple responsibilities. + * Efficient management of student information and tasks. +
+ +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). +* For the detailed documentation of this project, see the **[TAHub Website](https://ay2425s1-cs2103t-f13-1.github.io/tp/)**. diff --git a/build.gradle b/build.gradle index 0db3743584e..9c795d163cc 100644 --- a/build.gradle +++ b/build.gradle @@ -44,29 +44,35 @@ dependencies { String jUnitVersion = '5.4.0' String javaFxVersion = '17.0.7' + // JavaFX dependencies for specific platforms implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' - implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac-aarch64' // Changed to mac-aarch64 for Apple Silicon implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' - implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac-aarch64' // Changed to mac-aarch64 for Apple Silicon implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' - implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac-aarch64' // Changed to mac-aarch64 for Apple Silicon implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' - implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac-aarch64' // Changed to mac-aarch64 for Apple Silicon + // Other dependencies implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' + // JUnit dependencies testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion - testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'tahub.jar' +} + +run { + enableAssertions = true } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index ff3f04abd02..f68bda82dde 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,52 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Clarence Yeo - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/clarenceeey)] +[[portfolio](team/clarence.md)] -* Role: Project Advisor +* Role: Project Developer -### Jane Doe - +### notnotmax -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] + -* Role: Team Lead -* Responsibilities: UI +[[github](https://github.com/notnotmax)] +[[portfolio](team/notnotmax.md)] -### Johnny Doe +* Role: Developer +* Responsibilities: Deliverables, Deadlines, Scheduling and Tracking - +### Marcus Ang -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] + -* Role: Developer +[[github](http://github.com/marcusjhang)] +[[portfolio](team/marcusjhang.md)] + +* Role: Software Developer * Responsibilities: Data -### Jean Doe +### Han Yi - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/yhanyi)] +[[portfolio](team/yhanyi.md)] * Role: Developer * Responsibilities: Dev Ops + Threading -### James Doe +### Sky Lim Kai Yi - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/S-K-Y-Light)] +[[portfolio](team/sky.md)] -* Role: Developer -* Responsibilities: UI +* Role: Project Developer +* Responsibilities: Documentation diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 743c65a49d2..5c4b6591460 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -58,7 +58,7 @@ The *Sequence Diagram* below shows how the components interact with each other f Each of the four main components (also shown in the diagram above), * defines its *API* in an `interface` with the same name as the Component. -* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. +* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point). For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. @@ -72,7 +72,7 @@ The **API** of this component is specified in [`Ui.java`](https://github.com/se- ![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`, `StudentListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) @@ -81,15 +81,15 @@ 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 `Student`, `Consultation` & `Lesson` 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) -Here's a (partial) class diagram of the `Logic` component: +Here is a fuller diagram of how the `Logic` component might interact with adjacent classes: - + The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example. @@ -102,7 +102,7 @@ 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. The command can communicate with the `Model` when it is executed (e.g. to delete a person).
+1. The command can communicate with the `Model` when it is executed (e.g. to delete a student).
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`. @@ -122,12 +122,16 @@ How the parsing works: The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the address book data i.e., all `Student` objects (which are contained in a `UniqueStudentList` object). +* stores the currently 'selected' `Student` 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 `Consultation` objects (which are contained in a `UniqueConsultList` object). +* stores the currently 'selected' `Consultation` 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 `Lesson` objects (which are contained in a `UniqueLessonList` object). +* stores the currently 'selected' `Lesson` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Course` list in the `AddressBook`, which `Student` references. This allows `AddressBook` to only require one `Course` object per unique Course, instead of each `Student` needing their own `Course` objects.
@@ -155,6 +159,333 @@ Classes used by multiple components are in the `seedu.address.commons` package. This section describes some noteworthy details on how certain features are implemented. + +### Consultation Management + +The consultation management feature enables TAs to schedule and manage consultation sessions with students. This section describes the implementation details of the consultation system. + +#### Architecture + +The consultation feature comprises these key components: + +* `Consultation`: Core class representing a consultation session +* `Date`: Represents and validates consultation dates +* `Time`: Represents and validates consultation times +* `AddConsultCommand`: Handles adding new consultations +* `AddConsultCommandParser`: Parses user input for consultation commands + +The class diagram below shows the structure of the consultation feature: + + + +#### Implementation + +The consultation management system is implemented through several key mechanisms: + +**1. Date and Time Validation** + +The system enforces strict validation for consultation scheduling: +* Dates must be in `YYYY-MM-DD` format and represent valid calendar dates +* Times must be in 24-hour `HH:mm` format +* Both use Java's built-in `LocalDate` and `LocalTime` for validation + +Example: +```java +Date date = new Date("2024-10-20"); // Valid +Time time = new Time("14:00"); // Valid +Date invalidDate = new Date("2024-13-45"); // Throws IllegalArgumentException +``` + +**2. Consultation Management** + +The `Consultation` class manages: +* Immutable date and time properties +* Thread-safe student list management +* Equality based on date, time, and enrolled students + +Core operations: +```java +// Creating a consultation +Consultation consult = new Consultation(date, time, students); + +// Adding/removing students +consult.addStudent(student); +consult.removeStudent(student); + +// Getting immutable student list +List students = consult.getStudents(); // Returns unmodifiable list +``` +The sequence diagram below shows how the `addStudent(student)` method is performed in the `Consultation` class. + + + +The command first checks if the student is already in the consultation. If the student was already in the consultation, and exception is thrown and the student is not added. + +However, if the student was not previously in the consultation, the student is now added to the list of students in the consultation. + +**3. Command Processing** + +The system supports these consultation management commands: +- `addconsult`: Creates new consultation sessions +- `addtoconsult`: Adds students to existing consultations +- `deleteconsult`: Removes consultation sessions +- `removefromconsult`: Removes students from consultations + +Command examples: +``` +addconsult d/2024-10-20 t/14:00 +addtoconsult 1 n/John Doe i/3 +deleteconsult 1 +removefromconsult 1 n/John Doe +``` +The sequence diagram below shows how the command `addtoconsult 1 n/John Doe i/3` is executed. + + + +The commands for Consultations are executed using the `Logic` component: + +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., `AddToConsultCommandParser`) and uses it to parse the command. +1. This results in a `Command` object (in this case, the `AddToConsultCommand`) which is executed by the `LogicManager`. +1. The command can communicate with the `Model` when it is executed (e.g. to add a student to the consultation).
+ 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`. + + +**Aspect 1: Date and Time Representation** + +* **Alternative 1 (current choice)**: Separate `Date` and `Time` classes + * Pros: Clear separation of concerns, focused validation + * Cons: Two objects to manage instead of one + +* **Alternative 2**: Combined `DateTime` class + * Pros: Unified handling of temporal data + * Cons: More complex validation, reduced modularity + +**Aspect 2: Student List Management** + +* **Alternative 1 (current choice)**: Immutable view with mutable internal list + * Pros: Thread-safe external access, flexible internal updates + * Cons: Complex implementation + +* **Alternative 2**: Fully immutable list + * Pros: Simpler thread-safety + * Cons: Higher memory usage for modifications + +### Lesson Management + +The lesson management feature enables TAs to schedule and manage lessons with students. This section describes the implementation details of the lesson system. + +#### Architecture + +The lesson feature comprises these key components: + +* `Lesson`: Core class representing a lessons +* `Date`: Represents and validates lesson dates +* `Time`: Represents and validates lesson times +* `StudentLessonInfo`: Represents student info used for lessons. This includes the student's attendance and participation score for a lesson. +* `AddLessonCommand`: Handles adding new lessons +* `AddLessonCommandParser`: Parses user input for `AddLessonCommand` + +The class diagram below shows the structure of the lesson feature: + + + +#### Implementation + +The lesson management system is implemented through several key mechanisms: + +**1. Date and Time Validation** + +The system enforces strict validation for lesson scheduling: +* Dates must be in `YYYY-MM-DD` format and represent valid calendar dates +* Times must be in 24-hour `HH:mm` format +* Both use Java's built-in `LocalDate` and `LocalTime` for validation + +Example: +```java +Date date = new Date("2024-10-20"); // Valid +Time time = new Time("14:00"); // Valid +Date invalidDate = new Date("2024-13-45"); // Throws IllegalArgumentException +``` + +**2. Lesson Management** + +The `Lesson` class manages: +* Immutable date and time properties +* Thread-safe student information list management +* Equality based on date, time, and student information + +Core operations: +```java +// Creating a lesson +Lesson lesson = new Lesson(date, time, studentLessonInfoList); + +// Adding/removing students +lesson.addStudent(student); +lesson.removeStudent(student); + +// Setting attendance +lesson.setAttendance(student, attendance); + +// Setting pariticipation score +lesson.setParticipation(student, participationScore); + +// Getting immutable student info list, returns unmodifiable list +List studentInfoList = lesson.getStudentLessonInfoList(); +``` + +**3. Command Processing** + +The system supports these lesson management commands: +- `addlesson`: Creates new lesson +- `addtolesson`: Adds students to existing lesson +- `deletelesson`: Removes lesson +- `listlessons`: Lists all lessons +- `removefromlesson`: Removes students from lesson +- `marka`: Marks the attendance of students in a lesson (can mark as present or absent) +- `markp`: Sets the participation score of students in a lesson + +Command examples: +``` +addlesson d/2024-10-20 t/14:00 +addtolesson 1 n/John Doe i/3 +deletelesson 1 +removefromlesson 1 n/John Doe +markp 1 n/John Doe pt/25 +marka 1 n/John Doe n/Jane Doe a/y +``` +The sequence diagram below shows how the command `addtolesson 1 n/John Doe i/3` is executed. + + + +The commands for Lessons are executed using the `Logic` component, making it similar to the sequence diagram in the consultation section above. + +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., `AddToLessonCommandParser`) and uses it to parse the command. +1. This results in a `Command` object (in this case, the `AddToLessonCommand`) which is executed by the `LogicManager`. +1. The command can communicate with the `Model` when it is executed (e.g. to add a student to the lesson).
+ 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`. + + +The next sequence diagram shows how the `MarkLessonAttendanceCommand` is executed to update the attendance of students in a lesson. + + + +The flow of the program when `MarkLessonAttendanceCommand.execute` is called is as follows: + +1. When the execute method is called, the command first gets the `targetLesson` from the model. +2. The command then creates a new `Lesson` object (called `newLesson`) from the `targetLesson`. This creates a copy of the current `targetLesson`. +3. Now, for each student, the command sets the student's attendance using the `setAttendance` method in the `Lesson` class. +4. Finally, the command replaces the `targetLesson` in the model with the `newLesson`. + + +**Aspect 1: Date and Time Representation** + +* **Alternative 1 (current choice)**: Separate `Date` and `Time` classes + * Pros: Clear separation of concerns, focused validation + * Cons: Two objects to manage instead of one + +* **Alternative 2**: Combined `DateTime` class + * Pros: Unified handling of temporal data + * Cons: More complex validation, reduced modularity + +**Aspect 2: StudentInfo List Management** + +* **Alternative 1 (current choice)**: Immutable view with mutable internal list + * Pros: Thread-safe external access, flexible internal updates + * Cons: Complex implementation + +* **Alternative 2**: Fully immutable list + * Pros: Simpler thread-safety + * Cons: Higher memory usage for modifications + + +### Data Import / Export Feature + +The import/export feature allows TAs to archive and transfer their data in CSV format. This functionality is implemented for both students and consultations. + +#### Implementation + +The feature is implemented through four main command classes: +* `ExportCommand`: Exports student data to CSV +* `ExportConsultCommand`: Exports consultation data to CSV +* `ImportCommand`: Imports student data from CSV +* `ImportConsultCommand`: Imports consultation data from CSV + +Export File Handling: +* Files are always saved in the `data` directory first as the primary storage location +* The command then attempts to copy the file to the user's home directory as a backup +* If the home directory copy fails, the command still succeeds with only the data directory file +* The -f flag allows overwriting of existing files in both locations +* Filenames are restricted to alphanumeric characters to prevent path traversal attacks and ensure cross-platform compatibility + +Import File Resolution: + +The system searches for import files in this priority order to balance security and convenience: +* Data directory (./data/) - primary application storage +* Current directory - for convenience during testing/development +* Home directory paths (with ~) - for user convenience +* Absolute paths - for flexibility in file locations + +Each export command follows this workflow: +1. Validates filename input (must be alphanumeric)) +2. Creates `data` directory if needed +3. Writes data to CSV in `data` directory +4. Copies file to home directory if possible +5. Handles file overwrite with force flag (-f) +6. Returns success with one or both file paths + +Each import command follows this workflow: +1. Resolves file path using priority order: + - Data directory (./data/) + - Current directory + - Home directory paths (with ~) + - Absolute paths +2. Validates file exists and is readable +3. Parses CSV header +4. Process entries line by line +5. Logs invalid entries to `error.csv` + +Example sequences: +``` +Student CSV format: +Name,Phone,Email,Courses +John Doe,12345678,john@example.com,CS2103T;CS2101 + +Consultation CSV format: +Date,Time,Students +2024-10-20,14:00,John Doe;Jane Doe +``` + +#### Design Considerations + +**Aspect: File Format** +* **Alternative 1 (current choice)**: CSV format + * Pros: Widely compatible, human-readable, easy to edit + * Cons: Limited structure, requires careful escaping +* **Alternative 2**: JSON format + * Pros: Maintains data structure, less escaping needed + * Cons: Less human-readable, harder to edit manually + +**Aspect: Error Handling** +* **Alternative 1 (current choice)**: Log errors to separate file + * Pros: Clear error reporting, allows partial imports + * Cons: Requires managing separate error log file +* **Alternative 2**: Fail entire import on any error + * Pros: Ensures data consistency + * Cons: Less flexible, requires perfect input + +**Aspect: Export File Location** +* **Alternative 1 (current choice)**: Dual location with strict filenames + * Pros: Convenient access, automatic backup + * Cons: More complex validation, potential sync issues +* **Alternative 2**: Single location with flexible paths + * Pros: Simpler implementation, more user control + * Cons: Manual backup required + + +-------------------------------------------------------------------------------------------------------------------- + ### \[Proposed\] Undo/redo feature #### Proposed Implementation @@ -173,11 +504,11 @@ Step 1. The user launches the application for the first time. The `VersionedAddr ![UndoRedoState0](images/UndoRedoState0.png) -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +Step 2. The user executes `delete 5` command to delete the 5th student in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. ![UndoRedoState1](images/UndoRedoState1.png) -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +Step 3. The user executes `add n/David …​` to add a new student. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. ![UndoRedoState2](images/UndoRedoState2.png) @@ -185,7 +516,7 @@ Step 3. The user executes `add n/David …​` to add a new person. The `add` co
-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. +Step 4. The user now decides that adding the student 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) @@ -212,7 +543,7 @@ The `redo` command does the opposite — it calls `Model#redoAddressBook()`,
-Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +Step 5. The user then decides to execute the command `liststudents`. Commands that do not modify the address book, such as `liststudents`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. ![UndoRedoState4](images/UndoRedoState4.png) @@ -234,17 +565,9 @@ The following activity diagram summarizes what happens when a user executes a ne * **Alternative 2:** Individual command knows how to undo/redo by itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). + * Pros: Will use less memory (e.g. for `delete`, just save the student being deleted). * Cons: We must ensure that the implementation of each individual command are correct. -_{more aspects and alternatives to be added}_ - -### \[Proposed\] Data archiving - -_{Explain here how the data archiving feature will be implemented}_ - - --------------------------------------------------------------------------------------------------------------------- ## **Documentation, logging, testing, configuration, dev-ops** @@ -262,71 +585,702 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +Teaching Assistants (TAs) in academic institutions such as universities, colleges, and online learning platforms. -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +- Role: TAs supporting professors and lecturers in managing course-related tasks. +- Experience Level: New and experienced TAs handling multiple responsibilities. +- Needs: Efficient management of student information and tasks. +- Has a need to manage a significant number of contacts +- Prefer desktop apps over other types +- Can type fast +- Prefers typing to mouse interactions +- Is reasonably comfortable using CLI apps +**Value proposition**: TAHub simplifies the role of Teaching Assistants by providing a centralized hub to organize student information, and efficiently manage course-related tasks. This platform empowers TAs to focus more on enhancing student learning and less on administrative chaos. ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | +| Priority | As a … | I want to … | So that I can … | +|----------|--------------------|---------------------------------------------------------------------------|--------------------------------------------------------------------------| +| `* * *` | teaching assistant | add students to my course roster | keep track of all students under my supervision | +| `* * *` | teaching assistant | update student information | keep student records up-to-date and accurate | +| `* * *` | teaching assistant | search for a student by their name | quickly find a specific student when needed | +| `* * *` | teaching assistant | search for students enrolled in a course | quickly find all students enrolled in a certain course | +| `* * *` | teaching assistant | filter students by homework submission status | quickly get to grading and providing feedback | +| `* * *` | teaching assistant | mark students' attendance in my tutorial | leverage my fast typing to quickly take attendance | +| `* * *` | teaching assistant | assign participation marks to each student | keep track of student participation easily to assign a grade later | +| `* * *` | teaching assistant | export student data as a CSV | easily share information with professors or use in other applications | +| `* * *` | teaching assistant | schedule consultation sessions | set aside dedicated time slots to meet with students | +| `* * *` | teaching assistant | add students to consultation slots | keep track of which students are attending each consultation | +| `* * *` | teaching assistant | view all my upcoming consultations | prepare for and manage my consultation schedule | +| `* *` | teaching assistant | assign tasks and deadlines to students | track their responsibilities and ensure they stay on schedule | +| `* *` | teaching assistant | set reminders for important tasks or deadlines | stay notified of upcoming responsibilities and avoid missing them | +| `* *` | teaching assistant | view a calendar showing all upcoming student deadlines and my TA duties | manage my time effectively and avoid scheduling conflicts | +| `* *` | teaching assistant | filter students by academic performance | prioritise communication with students in need of help | +| `* *` | teaching assistant | manually tag students in need of help | remember to give these students additional support to help them catch up | +| `* *` | teaching assistant | filter students by their attendance | reach out to them if they have not attended for long periods of time | +| `* *` | teaching assistant | mark students who will be absent with a valid reason | keep track of special cases when taking attendance | +| `* *` | teaching assistant | search for the availability of students during a certain time | find the preferred timing to host a consultation session | +| `* *` | teaching assistant | view a list of students that match my search query without typing in full | handle mass search queries for convenience | +| `* *` | teaching assistant | merge duplicate student entries | maintain a clean and accurate database | +| `* *` | teaching assistant | backup my student database to a local file | ensure data safety and practice file management | +| `* *` | teaching assistant | use a command to import student data from a CSV file | quickly populate my database at the start of a semester | +| `* *` | teaching assistant | get alerts for consultation timing conflicts | avoid double booking consultation slots | +| `* *` | teaching assistant | see the history of consultations with each student | track how often I meet with specific students | +| `* *` | teaching assistant | add notes to consultation sessions | record what was discussed and any follow-up actions needed | +| `*` | teaching assistant | add notes to a student's profile | keep track of special considerations for each student | +| `*` | teaching assistant | assign a profile picture to each student | have a visual aid to recognise who is who in my tutorial | +| `*` | teaching assistant | toggle between light and dark mode | select my preferred viewing mode | +| `*` | teaching assistant | redesign the TAHub GUI layout | select my preferred visual layout | +| `*` | teaching assistant | press up and down to navigate command history | quickly reuse recently used commands | +| `*` | teaching assistant | generate a statistical summary of class performance | quickly assess overall class trends in scores/attendance etc. | +| `*` | teaching assistant | export my consultation schedule to my calendar | integrate consultation timings with my other appointments | +| `*` | teaching assistant | set recurring consultation slots | establish regular consultation hours without manual scheduling | *{More to be added}* ### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is the `TAHub` and the **Actor** is the `user`, unless specified otherwise) + +**Use case: UC1 - Add a student** + +**MSS:** +1. User requests to add a student by providing the necessary details (name, contact, courses, email). +2. TAHub validates the inputs. +3. TAHub adds the student with the provided details. +4. TAHub displays the updated student list. +
+Use case ends. + +**Extensions:** +* 2a. One or more input parameters are missing or invalid. + * 2a1. TAHub shows an error message indicating the missing or invalid field(s). +
+ Use case ends. +* 2b. Duplicate Student Exists (Student name matches an existing student). + * 2b1. TAHub shows a duplicate error message. +
+ Use case ends. -**Use case: Delete a person** -**MSS** +**Use case: UC2 - Find students by Course** -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 +**MSS:** +1. User requests to find students enrolled in a particular course. +2. TAHub shows a list of students enrolled in that particular course. +
+Use case ends. +**Extensions:** +* 1a. There are no students enrolled in the given course. + * 1a1. TAHub shows a message indicating there are no students found. +
+ Use case ends. +* 1b. There are multiple courses containing the given string as a prefix. + * 1b1. TAHub displays a list of all students enrolled in those courses. +
Use case ends. -**Extensions** -* 2a. The list is empty. +**Use case: UC3 - Find student by name** +**MSS:** +1. User requests to find a student by name. +2. TAHub displays a list of students whose names contain the input string as a prefix. +
+Use case ends. + +**Extensions:** +* 1a. The list is empty. + * 1a1. TAHub displays a message that there were no students found. +
Use case ends. -* 3a. The given index is invalid. +**Use case: UC4 - Edit Student Information** - * 3a1. AddressBook shows an error message. +**Precondition:** The Student exists in TAHub. - Use case resumes at step 2. +**MSS:** +1. User requests to edit a student's information by providing the index and necessary details (name, contact, courses, email). +2. TAHub validates the inputs. +3. TAHub updates the student with the provided details. +4. TAHub displays the updated student's information. +
+Use case ends. -*{More to be added}* +**Extensions:** +* 2a. One or more input parameters are missing or invalid. + * 2a1. TAHub shows an error message indicating the missing or invalid field(s). +
+ Use case ends. + + +**Use case: UC5 - Delete Student** + +**Precondition:** The Student exists in TAHub. + +**MSS:** + +1. User requests to delete a specific student in the list by index. +2. TAHub verifies the given index. +3. TAHub deletes the student at the index in the list. +
+Use case ends. + +**Extensions:** +* 2a. The given index is invalid. + * 2a1. TAHub shows an error message stating that the index is invalid. +
+ Use case ends. + + +**Use case: UC6 - Export Student Data** + +**MSS:** +1. User requests to export student data with desired filename. +2. TAHub validates filename. +3. TAHub creates CSV file in data directory. +4. TAHub copies file to home directory. +5. TAHub shows number of students exported. + +**Extensions:** +* 2a. Invalid filename + * 2a1. TAHub shows error message about invalid characters. +
+ Use case ends. +* 3a. File already exists. + * 3a1. TAHub shows error message suggesting force flag. +
+ Use case ends. +* 3b. Directory creation fails. + * 3b1. TAHub shows error message about directory creation. +
+ Use case ends. +* 4a. Write operation fails. + * 4a1. TAHub shows error message about write failure. +
+ Use case ends. + + +**Use case: UC7 - Import Student Data** + +**MSS:** +1. User requests to import student data with CSV filename. +2. TAHub validates file exists and is readable. +3. TAHub reads CSV header and validates format. +4. TAHub processes each row and adds valid students. +5. TAHub shows number of students imported and any errors. +
+Use case ends. + +**Extensions:** +* 2a. File not found. + * 2a1. TAHub shows error message about missing file. +
+ Use case ends. + +* 3a. Invalid header format. + * 3a1. TAHub shows error message about expected format. +
+ Use case ends. + +* 4a. Invalid data in rows. + * 4a1. TAHub logs invalid entries to error.csv. + * 4a2. TAHub continues processing remaining rows. + * 4a3. Success message includes count of errors. +
+ Use case ends. + + +**Use case: UC8 - Refresh Student List** + +**Guarantees:** +1. Overall Student List will be displayed. + +**MSS:** +1. User requests to refresh student list. +2. TAHub refreshes and displays the student list. +
+Use case ends. + + +**Use case: UC9 - Create a Consultation** + +**MSS:** +1. User requests to add a consultation by providing the necessary details (date, time). +2. TAHub validates the inputs. +3. TAHub adds the consultation with the provided details. +4. TAHub displays the updated consultation list. +
+Use case ends. + +**Extensions:** +* 2a. One or more input parameters are missing or invalid. + * 2a1. TAHub shows an error message indicating the missing or invalid fields. +
+ Use case ends. +* 2b. Duplicate consultation Exists (Consultation date & time matches an existing consultation) + * 2b1. TAHub shows a duplicate error message. +
+ Use case ends. + + +**Use case: UC10 - Add Student to Consultation** + +**Precondition:** The Consultation exists in TAHub. + +**MSS:** +1. User requests to add a specific student to the consultation by providing the necessary details (Consultation Index, Student Index, Student Name) +2. TAHub validates the inputs. +3. TAHub adds the student to the consultation. +4. TAHub displays the updated consultation list. +
+Use case ends. + +**Extensions:** +* 2a. Invalid Consultation Index. + * 2a1. TAHub shows an error message stating that the Consultation Index is invalid. +
+ Use case ends. +* 2b. Invalid Student Index. + * 2b1. TAHub shows an error message stating that the Student Index is invalid. +
+ Use case ends. +* 2c. Student Name not Found in Student List. + * 2c1. TAHub shows an error message stating that the Student does not exist. +
+ Use case ends. +* 2d. Student is already in consultation + * 2d1. TAHub shows an error message stating that the Student is already in the consultation. +
+ Use case ends. + + +**Use case: UC11 - Remove Student from Consultation** + +**Precondition:** The Consultation exists in TAHub. + +**MSS:** +1. User requests to remove a specific student from the consultation by providing the necessary details (Consultation Index, Student Name) +2. TAHub validates the inputs. +3. TAHub removes the student from the consultation. +4. TAHub displays the updated consultation list. +
+ Use case ends. + +**Extensions:** +* 2a. Invalid Consultation Index. + * 2a1. TAHub shows an error message stating that the Consultation Index is invalid. +
+ Use case ends. +* 2b. Student Name not Found in Student List. + * 2b1. TAHub shows an error message stating that the Student does not exist. +
+ Use case ends. +* 2c. Student is not in consultation. + * 2c1. TAHub shows an error message stating that the Student is not in the consultation. +
+ Use case ends. + + +**Use case: UC12 - Delete Consultation** + +**Precondition:** The Consultation exists in TAHub. + +**MSS:** +1. User requests to delete a specific consultation by providing the necessary details (Consultation Index). +2. TAHub verifies the Consultation Index. +3. TAHub deletes the consultation at the index in the Consultation List. +
+Use case ends. + +**Extensions:** +* 2a. Invalid Consultation Index. + * 2a1. TAHub shows an error message stating that the Consultation Index is invalid. +
+ Use case ends. + + +**Use case: UC13 - Export Consultation Data** + +**MSS:** +1. User requests to export consultation data with desired filename. +2. TAHub validates filename. +3. TAHub creates CSV file with consultation data. +4. TAHub copies file to home directory. +5. Success message shows number of consultations exported. +
+Use case ends. + +**Extensions:** +* [Same extensions as UC6] + + +**Use case: UC14 - Import Consultation Data** + +**MSS:** +1. User requests to import consultation data with CSV filename. +2. TAHub validates file exists and is readable. +3. TAHub reads CSV header and validates format. +4. TAHub processes each row: + - Validates date and time format. + - Checks student existence in TAHub. + - Creates consultation entries. +5. TAHub shows number of consultations imported and any errors. +
+Use case ends. + +**Extensions:** +* 2a. File not found. + * 2a1. TAHub shows error message about missing file. +
+ Use case ends. +* 3a. Invalid header format. + * 3a1. TAHub shows error message about expected format +
+ Use case ends. + +* 4a. Invalid data in rows + * 4a1. TAHub logs invalid entries to error.csv. + * 4a2. TAHub continues processing remaining rows. + * 4a3. Error types include: + - Invalid date/time format. + - Student not found in TAHub. + - Duplicate consultation. + * 4a4. Success message includes count of errors. +
+ Use case ends. + + +**Use case: UC15 - Refresh Consultation List** + +**Guarantees:** +1. Overall Consultation List will be displayed. + +**MSS:** +1. User requests to refresh consultation list. +2. TAHub refreshes and displays the consultation list. +
+ Use case ends. + + +**Use case: UC16 - Create a Lesson** + +**MSS:** +1. User requests to add a lesson by providing the necessary details (date, time). +2. TAHub validates the inputs. +3. TAHub adds the lesson with the provided details. +4. TAHub displays the updated lesson list. +
+ Use case ends. + +**Extensions:** +* 2a. One or more input parameters are missing or invalid. + * 2a1. TAHub shows an error message indicating the missing or invalid fields. +
+ Use case ends +* 2b. Duplicate Lesson Exists (Lesson date & time matches an existing lesson) + * 2b1. TAHub shows a duplicate error message. +
+ Use case ends. + + +**Use case: UC17 - Add Student to Lesson** + +**Precondition:** The Lesson exists in TAHub. + +**MSS:** +1. User requests to add a specific student to the lesson by providing the necessary details (Lesson Index, Student Index, Student Name) +2. TAHub validates the inputs. +3. TAHub adds the student to the lesson. +4. TAHub displays the updated lesson list. +
+ Use case ends. + +**Extensions:** +* 2a. Invalid Lesson Index. + * 2a1. TAHub shows an error message stating that the Lesson Index is invalid. +
+ Use case ends. +* 2b. Invalid Student Index. + * 2b1. TAHub shows an error message stating that the Student Index is invalid. +
+ Use case ends. +* 2c. Student Name not Found in Student List. + * 2c1. TAHub shows an error message stating that the Student does not exist. +
+ Use case ends. +* 2d. Student is already in lesson. + * 2d1. TAHub shows an error message stating that the Student is already in the lesson. +
+ Use case ends. + + +**Use case: UC18 - Remove Student from Lesson** + +**Precondition:** The Lesson exists in TAHub. + +**MSS:** +1. User requests to remove a specific student from the lesson by providing the necessary details (Lesson Index, Student Name) +2. TAHub validates the inputs. +3. TAHub removes the student from the lesson. +4. TAHub displays the updated lesson list. +
+ Use case ends. + +**Extensions:** +* 2a. Invalid Lesson Index. + * 2a1. TAHub shows an error message stating that the Lesson Index is invalid. +
+ Use case ends. +* 2b. Student Name not Found in Student List. + * 2b1. TAHub shows an error message stating that the Student does not exist. +
+ Use case ends. +* 2c. Student is not in lesson + * 2c1. TAHub shows an error message stating that the Student is not in the lesson. +
+ Use case ends. + + +**Use case: UC19 - Delete Lesson** + +**Precondition:** The Lesson exists in TAHub. + +**MSS:** +1. User requests to delete a specific lesson by providing the necessary details (Lesson Index). +2. TAHub verifies the Lesson Index. +3. TAHub deletes the lesson at the index in the Lesson List. +
+ Use case ends. + +**Extensions:** +* 2a. Invalid Lesson Index + * 2a1. TAHub shows an error message stating that the Lesson Index is invalid. +
+ Use case ends. + + +**Use case: UC20 - Mark Student Attendance in Lesson** + +**Precondition:** The Student & Lesson exists in TAHub. Student is already in the Lesson. + +**MSS:** +1. User requests to mark a specific student's attendance in the lesson by providing the necessary details (Lesson Index, Student Name, Attendance) +2. TAHub validates the inputs. +3. TAHub marks the student's attendance in the lesson. +4. TAHub displays the updated student attendance. +
+ Use case ends. + +**Extensions:** +* 2a. Invalid Lesson Index. + * 2a1. TAHub shows an error message stating that the Lesson Index is invalid. +
+ Use case ends. +* 2b. Student is not in lesson. + * 2b1. TAHub shows an error message stating that the Student is not in the lesson. +
+ Use case ends. +* 2c. Invalid Attendance. + * 2c1. TAHub shows an error message stating that the Attendance is invalid. +
+ Use case ends. + + +**Use case: UC21 - Mark Student Participation in Lesson** + +**Precondition:** The Student & Lesson exists in TAHub. Student is already in the Lesson. + +**MSS:** +1. User requests to mark a specific student's participation in the lesson by providing the necessary details (Lesson Index, Student Name, Participation) +2. TAHub validates the inputs. +3. TAHub marks the student's participation in the lesson. +4. TAHub displays the updated student attendance & participation. +
+ Use case ends. + +**Extensions:** +* 2a. Invalid Lesson Index. + * 2a1. TAHub shows an error message stating that the Lesson Index is invalid. +
+ Use case ends. +* 2b. Student is not in lesson. + * 2b1. TAHub shows an error message stating that the Student is not in the lesson. +
+ Use case ends. +* 2c. Invalid Participation. + * 2c1. TAHub shows an error message stating that the Participation is invalid. +
+ Use case ends. +* 2d. Participation Score is Positive & Valid. + * 2d1. TAHub marks the student's attendance as Present. +
+ Use case resumes from step 3. + + +**Use case: UC22 - Refresh Lesson List** + +**Guarantees:** +1. Overall Lesson List will be displayed. + +**MSS:** +1. User requests to refresh lesson list. +2. TAHub refreshes and displays the lesson list. +
+ 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. +2. Should be able to hold up to 1000 students without a noticeable sluggishness in performance for typical usage. 3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +4. The system should be able to scale to accommodate a growing number of users (teachers, TAs, students) without requiring significant reengineering. +5. The platform should have a clean, intuitive user interface that allows new users to complete basic tasks (like searching for a student) with minimal training. +6. Any search query should return results within 1 second for up to 10,000 student records. +7. Product should be for a single user. +8. The data should be stored locally and should be in a human editable text file. +9. The software should work without requiring an installer. -*{More to be added}* +### Planned Enhancements + +**Make command names case-insensitive** + +**Description** + +Make command names case-insensitive, i.e. `Find` is safe to substitute for +`find` in the `find` command. + +**Rationale** + +Users may make small mistakes in capitalisation when typing quickly. +It can save time to ensure minor mistakes such as these in the command keyword +(not the arguments) will not prevent the command from working. + +**Allow students with same name to be added** + +**Description** + +Allow students with the same name to be added (still case-sensitive).
+Instead, disallow students with the same **phone number** or **email address** to be added.
+Commands that currently use student **names** as arguments should instead use their **index** +in the student list. + +**Rationale** + +It is arguably rarer for students to share a phone number or email address (university email?) with +another student than it is to share a name. + +**Allow more commands to use student index instead of full name** + +**Description** + +Currently, some commands such as `marka` and `markp` require the user to type +out the full name of the student.
+Instead, change it so that they use the student's index in the student list, similar to commands +like `addtolesson`. + +**Rationale** + +Under normal conditions, it is impossible for a consultation or lesson to have a student +that is not in the student list. Thus, it is safe to specify students by their index +in the student list.
+Additionally, doing so is faster to type. + +**Allow certain special characters to be used in names** + +**Description** + +Allow more special characters such as `/` and `-` to be used in student names. + +**Rationale** + +It is possible for students' legal names to contain `-` (e.g. Mary-Ann) or `/` (e.g. S/O). +Relaxing current restrictions to allow such characters will allow such names to be input. + +**Add clearer error message for integers/indexes ** + +**Description** + +Currently, when an invalid or sufficiently large number is given as an index, the error message says:
+Index is not an unsigned non-zero integer. +This should be changed to specify the requirement that indexes should be between +1 and `Integer.MAX_VALUE`. + +**Rationale** + +The current error message can be confusing for non-technical users who do not know what +*unsigned* means, and misleading when it also shows for large inputs that exceed Java's +integer limit, such as `104890385925902379`. +Clearer error messages can help to mitigate such confusion. + +**Make `find c/` throw an error** + +**Description** + +Currently, `find c/` does not throw an error. Instead, it runs successfully but always +returns 0 students. +This should be changed so an appropriate error message is shown (courses cannot be empty). + +**Rationale** + +A user might expect `find c/` to find students who are taking no courses. +However, this is not the case, and will result in confusion.
+Hence, this command should not execute successfully. + +**Make participation not accept + before the number** + +**Description** + +Currently, the participation argument in the `markp` command accepts the use of `+` before it, +i.e. `+3` is treated as `3`. +This should be treated as an invalid format. + +**Rationale** + +Though not strictly wrong, the index parser currently already checks for `+` and treats it as invalid. +For consistency, this should also apply to participation. + +**Make year 0000 an illegal value for the date** + +**Description** + +Currently, year 0000 is accepted as a valid year. +This should be changed to no longer be valid. + +**Rationale** + +According to Wikipedia, a year 0 does not exist in the Anno Domini calendar year. +Therefore, it should not be allowed. + +**Standardise error messages involving index** + +**Description** + +For some commands, entering specific indexes (like 0) will show an error message +stating that the index is invalid (not an unsigned nonzero integer), while other times +it will show the default error message for incorrect format. + +**Rationale** + +These error messages should be standardised to avoid confusion. +Any errors when parsing the index specifically ideally should specify that the index +specifically is invalid to help the user correct it. ### Glossary +* **Attendance**: Student's Presence/Absence for a Lesson +* **Consultation**: A scheduled meeting between TA and students for academic discussions +* **Lesson**: An Official Tutorial/Lab coordinated by TA * **Mainstream OS**: Windows, Linux, Unix, MacOS +* **Participation**: Student's Score for performance in a Lesson * **Private contact detail**: A contact detail that is not meant to be shared with others +* **Student***: A Person with Name, Contact, Email & Courses +* **Student Record**: A collection of data fields that stores information about a student, including their name, contact information, course enrollment +* **TA**: Teaching Assistant -------------------------------------------------------------------------------------------------------------------- @@ -345,38 +1299,326 @@ testers are expected to do more *exploratory* testing. 1. Download the jar file and copy into an empty folder - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 2. 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 +2. Saving window preferences 1. Resize the window to an optimum size. Move the window to a different location. Close the window. - 1. Re-launch the app by double-clicking the jar file.
+ 2. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ +### Student Test Cases + +### Adding a student +1. Test case: `add n/TestOne p/11111111 e/test1@example.com c/CS2103T`
+ Expected: Student `TestOne` is added to the list. Details of the added student is shown. + +2. Test case: `add n/TestOne p/11111111`
+ Expected: No student is added. Error details shown. + +3. Test case: `add n/TestOne e/test1@example.com c/CS2103T`
+ Expected: No student is added. Error details shown. + +4. Test case: `add n/Test1 p/11111111 e/test1@example.com c/CS2103T`
+ Expected: No student is added. Error details shown. + +### Finding a student (by course) +1. Prerequisites: List all students using the `liststudents` command. Multiple students in the list. + +2. Test case: `find c/CS2103T` (Assuming Students with course `CS2103T` Exist)
+ Expected: Displays students details with course `CS2103T`. + +3. Test case: `find c/CS2103T` (Assuming Students with course `CS2103T` does not Exist)
+ Expected: No Students Found. Displays 0 students. -### Deleting a person +4. Test case: `find c/1234` + Expected: No Students Found. Error details shown. -1. Deleting a person while all persons are being shown - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +### Finding a student (by name) +1. Prerequisites: List all students using the `liststudents` command. Multiple students in the list. - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. +2. Test case: `find n/TestOne` (Assuming Student with name `TestOne` Exists)
+ Expected: Displays students details with name `TestOne`. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. +3. Test case: `find n/TestOne` (Assuming Students with name `TestOne` does not Exist)
+ Expected: No Students Found. Displays 0 students. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. +4. Test case: `find n/Test1` + Expected: No Students Found. Error details shown. -1. _{ more test cases …​ }_ +### Editing a student +1. Prerequisites: List all students using the `liststudents` command. Multiple students in the list. + +2. Test case: `edit 1 n/TestOne p/11111111`
+ Expected: 1st student is edited. Details of the edited student is shown. + +3. Test case: `edit 2 e/test1@example.com c/CS2103T`
+ Expected: 2nd student is edited. Details of the edited student is shown. + +4. Test case: `edit 2 n/Test 2`
+ Expected: No student is edited. Error details shown. + +5. Other incorrect edit commands to try: `edit`, `edit x`, `...` (where x is larger than the list size)
+ Expected: No student is edited. Error details shown. + +### Deleting a student +1. Prerequisites: List all students using the `liststudents` command. Multiple students in the list. + +2. Test case: `delete 1`
+ Expected: 1st student is deleted from the list. Details of the deleted student shown in the status message. + +3. Test case: `delete 0`
+ Expected: No student is deleted. Error details shown in the status message. + +4. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ Expected: No student is deleted. Error details shown in the status message. + +### Lesson Test Cases + +### Adding a lesson: `addlesson` + +1. Test case: `addlesson d/2024-10-20 t/14:00` + Expected: Lesson with date 2024-10-20 and time 14:00 is added to the list. Details of the added lesson are shown. + +2. Test case: `addlesson d/2024-10-20` + Expected: No lesson is added. Error details shown. + +3. Test case: `addlesson t/14:00` + Expected: No lesson is added. Error details shown. + +4. Test case: `addlesson d/2024-10-20 t/14:00` (when a lesson with the same date and time already exists) + Expected: No lesson is added. Error message about duplicate lesson shown. + +### Adding a student to a lesson: `addtolesson` + +1. Prerequisites: At least one lesson and one student exist in the list. + +2. Test case: `addtolesson 1 n/TestOne` + Expected: Student TestOne is added to the first lesson. Confirmation message is shown. + +3. Test case: `addtolesson 1 i/2` (assuming the student at index 2 exists) + Expected: The student at index 2 is added to the first lesson. Confirmation message is shown. + +4. Test case: `addtolesson 1 n/NonExistentStudent` + Expected: No student is added. Error message about student not found shown. + +5. Test case: `addtolesson 2 n/TestOne` (where lesson at index 2 doesn’t exist) + Expected: No student is added. Error details shown. + +6. Test case: `addtolesson 1 n/TestOne i/2` + Expected: Both TestOne and the student at index 2 are added to the first lesson. Confirmation message is shown. + + +### Removing a student from a lesson: `removefromlesson` + +1. Prerequisites: At least one lesson with one or more students exists in the list. + +2. Test case: `removefromlesson 1 n/TestOne` + Expected: Student TestOne is removed from the first lesson. Confirmation message is shown. + +3. Test case: `removefromlesson 1 n/NonExistentStudent` + Expected: No student is removed. Error message about student not found shown. + +4. Test case: `removefromlesson 2 n/TestOne` (where lesson at index 2 doesn’t exist) + Expected: No student is removed. Error details shown. + +### Marking attendance in a lesson: `marka` + +1. Test case: `marka 1 n/TestOne a/1` + Expected: Student `TestOne` is marked as attended for the first lesson. Confirmation message is shown. + +2. Test case: `marka 1 n/TestOne a/0` + Expected: Student `TestOne` is marked as absent for the first lesson. Confirmation message is shown. + +3. Test case: `marka 1 n/John Doe n/Jane Doe a/1` + Expected: Students `John Doe` and `Jane Doe` are both marked as attended for the first lesson. Confirmation message is shown. + +4. Test case: `marka 1 n/John Doe n/Jane Doe a/0` + Expected: Students `John Doe` and `Jane Doe` are both marked as absent for the first lesson. Confirmation message is shown. + +5. Test case: `marka 1 n/NonExistentStudent a/1` + Expected: No attendance is marked. Error message about student not found is shown. + +6. Test case: `marka 2 n/TestOne a/1` (where lesson at index 2 doesn’t exist) + Expected: No attendance is marked. Error message about lesson not found is shown. + +### Marking participation in a lesson: `markp` + +1. Test case: `markp 1 n/TestOne pt/10` + Expected: Student `TestOne` is marked with a participation score of 10 for the first lesson, and attendance is set to true. Confirmation message is shown. + +2. Test case: `markp 1 n/TestOne pt/0` + Expected: Student `TestOne` is marked with a participation score of 0 for the first lesson. Attendance remains unchanged. Confirmation message is shown. + +3. Test case: `markp 1 n/John Doe n/Jane Doe pt/15` + Expected: Students `John Doe` and `Jane Doe` are both marked with a participation score of 15 for the first lesson, and their attendance is set to true. Confirmation message is shown. + +4. Test case: `markp 1 n/NonExistentStudent pt/10` + Expected: No participation is marked. Error message about student not found is shown. + +5. Test case: `markp 2 n/TestOne pt/10` (where lesson at index 2 doesn’t exist) + Expected: No participation is marked. Error message about lesson not found is shown. + +6. Test case: `markp 1 n/TestOne pt/101` + Expected: No participation is marked. Error message about invalid participation score is shown (as the score exceeds the valid range of 0-100). + +7. Test case: `markp 1 n/TestOne pt/-1` + Expected: No participation is marked. Error message about invalid participation score is shown (as the score is below the valid range of 0-100). + +### Removing a lesson: `deletelesson` + +1. Prerequisites: At least one lesson exists in the list. + +2. Test case: `deletelesson 1` + Expected: The first lesson is removed from the list. Confirmation message is shown. + +3. Test case: `deletelesson 0` + Expected: No lesson is removed. Error details shown. + +4. Other incorrect remove commands: `deletelesson`, `deletelesson x` (where x is larger than the list size) + Expected: No lesson is removed. Error details shown. + +5. Test case: `deletelesson 1;2` + Expected: Both the 1st and 2nd lessons are deleted from the list. Confirmation message is shown. + +### Consultation Test Cases + +### Adding a consultation: `addconsult` + +1. Test case: `addconsult d/2024-10-20 t/14:00` + Expected: Consultation on 2024-10-20 at 14:00 is added. Confirmation message is shown. + +2. Test case: `addconsult d/2024-10-20` + Expected: No consultation is added. Error details shown. + +3. Test case: `addconsult t/14:00` + Expected: No consultation is added. Error details shown. + +4. Test case: `addconsult d/2024-10-20 t/14:00` (when a consultation with the same date and time exists) + Expected: No consultation is added. Error message about duplicate consultation shown. + +### Listing all consultations: `listconsults` + +1. Prerequisites: At least one consultation exists. +2. Test case: `listconsults` + Expected: Displays a list of all consultations. + +### Adding students to a consultation: `addtoconsult` + +1. Prerequisites: At least one consultation and one student exist. + +2. Test case: `addtoconsult 1 n/TestOne` + Expected: Student TestOne is added to the first consultation. Confirmation message is shown. + +3. Test case: `addtoconsult 1 i/2` (assuming the student at index 2 exists) + Expected: The student at index 2 is added to the first consultation. Confirmation message is shown. + +4. Test case: `addtoconsult 1 n/NonExistentStudent` + Expected: No student is added. Error message about student not found shown. + +5. Test case: `addtoconsult 2 n/TestOne` (where consultation at index 2 doesn’t exist) + Expected: No student is added. Error details shown. + +6. Test case: `addtoconsult 1 n/TestOne i/2` + Expected: Both TestOne and the student at index 2 are added to the first consultation. Confirmation message is shown. + +### Removing students from a consultation: `removefromconsult` + +1. Prerequisites: At least one consultation with one or more students exists. + +2. Test case: `removefromconsult 1 n/TestOne` + Expected: Student TestOne is removed from the first consultation. Confirmation message is shown. + +3. Test case: `removefromconsult 1 n/NonExistentStudent` + Expected: No student is removed. Error message about student not found shown. + +4. Test case: `removefromconsult 2 n/TestOne` (where consultation at index 2 doesn’t exist) + Expected: No student is removed. Error details shown. + +### Deleting consultations: `deleteconsult` + +1. Prerequisites: At least one consultation exists. + +2. Test case: `deleteconsult 1` + Expected: The first consultation is deleted. Confirmation message is shown. + +3. Test case: `deleteconsult 0` + Expected: No consultation is deleted. Error details shown. + +4. Other incorrect delete commands: `deleteconsult`, `deleteconsult x` (where x is larger than the list size) + Expected: No consultation is deleted. Error details shown. + +5. Test case: `deleteconsult 1;2` + Expected: Both the 1st and 2nd consultations are deleted from the list. Confirmation message is shown. ### Saving data 1. Dealing with missing/corrupted data files - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. If you encounter an unexpected empty TAHub (no students, consults or lessons) upon startup or your data +is replaced by sample data, your data file may be corrupted. + 2. If you wish to try and salvage your data, **do not** perform any command yet. **This will overwrite your data.** + 3. Copy your data file to make a safe backup first, and rename it something other than `addressbook`. You can open +this file to view your data in JSON format. + 4. TAHub will generate a new data file with sample data. In the meantime, if you are experienced +with JSON, you can attempt to recover your data file by fixing issues in the file, usually syntax/formatting. + +### Exporting data + +#### Exporting student data +1. Prerequisites: List all students using the `liststudents` command. Multiple students should be in the list. + +2. Test case: `export students`
+ Expected: CSV file created in data directory and home directory. Success message shows number of students exported. + +3. Test case: `export test.file`
+ Expected: Error message about invalid filename characters. + +4. Test case: `export students` (when students.csv already exists)
+ Expected: Error message suggesting force flag usage. + +5. Test case: `export -f students`
+ Expected: Existing file overwritten. Success message shows number of students exported. + +#### Exporting consultation data +1. Prerequisites: List all consultations using the `listconsults` command. Multiple consultations should be in the list. + +2. Test case: `exportconsult sessions`
+ Expected: CSV file created with consultation data. Success message shows number of consultations exported. + +3. Test case: `exportconsult sessions` (when file exists)
+ Expected: Error message suggesting force flag usage. + +4. Other incorrect export commands to try: `exportconsult`, `exportconsult /test`, `exportconsult test.csv`
+ Expected: Error messages about invalid format/filename. + +### Importing data + +#### Importing student data +1. Prerequisites: Prepare a valid CSV file with header "Name,Phone,Email,Courses" + +2. Test case: `import students.csv` (with valid data)
+ Expected: Students imported successfully. Success message shows number of students imported. + +3. Test case: `import nonexistent.csv`
+ Expected: Error message about file not found. + +4. Test case: Import file with invalid rows (wrong format, duplicate students)
+ Expected: Some students imported. Error.csv created with invalid entries. Success message shows counts of successes and failures. + +#### Importing consultation data +1. Prerequisites: Prepare a valid CSV file with header "Date,Time,Students" + +2. Test case: `importconsult sessions.csv` (with valid data)
+ Expected: Consultations imported successfully. Success message shows number of consultations imported. + +3. Test case: Import file with invalid dates or times
+ Expected: Invalid entries logged to error.csv. Success message shows counts. + +4. Test case: Import file with nonexistent students
+ Expected: Entries with invalid students logged to error.csv. Success message shows counts. -1. _{ more test cases …​ }_ +5. Other incorrect import commands to try: `importconsult`, `importconsult /test.csv`, `importconsult ../test.csv`
+ Expected: Error messages about invalid format or file location. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 84b4ddc4e40..d23c03e53f5 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,7 +3,10 @@ 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. +TAHub is a **desktop app for managing students, 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, TAHub can get your student management tasks done faster than traditional GUI apps. + +TAHub simplifies the role of Teaching Assistants by providing a centralized hub to organize student information, and efficiently manage course-related tasks. This platform empowers TAs to focus more on enhancing student learning and less on administrative chaos. * Table of Contents {:toc} @@ -12,158 +15,557 @@ 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. +1. Ensure you have Java `17` or above installed in your Computer.
+To check your Computer's Java Version, see this [guide](https://www.wikihow.com/Check-Your-Java-Version-in-the-Windows-Command-Line).
+If you don't have Java, see this installation [guide](https://docs.oracle.com/en/java/javase/23/install/overview-jdk-installation.html).
+ + +2. Download the latest `.jar` file from [here](https://github.com/AY2425S1-CS2103T-F13-1/tp/releases).
+ Scroll down until you see this part. Click on `tahub.jar` to download it. + ![img](images/UgQuickStartDownload.png) + + +3. Copy the file to the folder you want to use as the _home folder_ for your TAHub.
+ The _home folder_ is where other files such as the data file will be created. -1. Download the latest `.jar` file from [here](https://github.com/se-edu/addressbook-level3/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +4. Open a command terminal and `cd` into the folder you put the jar file in.
+ If you don't know what that means, fret not - see [here](https://www.wikihow.com/Change-Directories-in-Command-Prompt). + + +5. Type `java -jar tahub.jar` and hit enter.
+ A GUI similar to the below should appear in a few seconds.
+ Note how the app contains some sample data.
+ -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
![Ui](images/Ui.png) -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try: - * `list` : Lists all contacts. +6. 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: - * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. + * `add n/John Doe p/98765432 e/johnd@example.com c/CS2103T;CS2101` : Adds a student named `John Doe` to TAHub. - * `delete 3` : Deletes the 3rd contact shown in the current list. + * `delete 2` : Deletes the 2nd student shown in the current student list. - * `clear` : Deletes all contacts. + * `clear` : Deletes all students, consultations & lessons. * `exit` : Exits the app. -1. Refer to the [Features](#features) below for details of each command. + +7. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- -## Features +# Features
-**:information_source: Notes about the command format:**
+**How do I read the command format?**
+ +* Main command keywords are **case-sensitive** unless specified otherwise.
+ e.g. `Find` and `Add` do not work for `find` and `add` respectively. * Words in `UPPER_CASE` are the parameters to be supplied by the user.
e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. * Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. + e.g `n/NAME [c/COURSE]` can be used as `n/John Doe c/CS2103T` or as `n/John Doe`. -* Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. +* Items with `…` after them can be used multiple times including zero times.
+ e.g. `[c/COURSE]…` can be used as ` ` (i.e. 0 times), `c/CS2103T;CS2101` +(note that use of `;` is unique to only some fields), `c/CS2103T c/CS2101` etc. * Parameters can be in any order.
e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
+* Extraneous parameters for commands that do not take in parameters (such as `help`, `liststudents`, `exit` and `clear`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. * 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.
+**Additional Notes** + +Here are more detailed notes regarding certain parameters that apply to all commands unless specified otherwise. + +* When specifying an index, leading zeroes are ignored, i.e. `001` is equivalent to `1`. + +* When specifying an index, it should be between 1 and 2147483647, inclusive. The current error message +may not reflect this requirement, but changes for clarification are planned in the future. + +* When targeting a student by their index, TAHub uses the indexes **as currently displayed** on the +student list. For example, if you run a `find` command that only lists one student out of originally five, trying +to select a student at index 2 will fail. + +## General Commands + ### Viewing help : `help` -Shows a message explaning how to access the help page. +Shows a message explaining how to access the help page. ![help message](images/helpMessage.png) Format: `help` +### Clearing all entries : `clear` + +Clears all entries from TAHub. + +Format: `clear` + +### Exiting the program : `exit` + +Exits the program. + +Format: `exit` + +# Students + +The student list is shown on the left side of TAHub.
+Currently, you can: +- Add, Edit and delete student +- Find Students by Name or Course +- Export & Import Student Data + +In a student, courses are represented by course tags. +Name, Phone Number & Email are represented as texts.
+ +![UgStudent.png](images/UgStudent.png) -### Adding a person: `add` +This is an example of a student. In this example, +the student's name is `Fyodor Dostoevsky`, +studying courses CS2100, CS2101, CS2103T, HSS1000, MA2104 & MA2108, +has Phone Number 98765432 and Email fyodor@gmail.com. -Adds a person to the address book. +## Student Commands -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +### Adding a student: `add` + +Adds a student to TAHub. + +Format: `add n/NAME p/PHONE_NUMBER e/EMAIL [c/COURSE]…`
:bulb: **Tip:** -A person can have any number of tags (including 0) +A student can have any number of courses (including 0)
-Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* `NAME` must only contain alphabetical characters, commas, and spaces. +* `NAME` must be unique, i.e. no two students can have the same name. + * However, names are **case-sensitive**, i.e. `John Doe` is considered different from `john doe`. +* `PHONE_NUMBER` should only contain numbers, and should be at least 3 numbers long. +* `EMAIL` should conform to the format `local-part@domain` and: + * `local-part` should only contain alphanumeric characters and these special characters: `+` `_` `.` `-`. + * `local-part` must start and end with alphanumeric characters. + * `domain` must end with a domain label at least 2 characters long. + * `domain` must have each domain label start and end with alphanumeric characters. + * `domain` must have each domain label consist only of alphanumeric characters separated only if hyphens (if any). +* `COURSE`s should begin with 2-4 letters, followed by 4 digits, followed by 0-2 letters. +Example: `MA1100`, `GEA1000N`, `GESS1000T` etc. -### Listing all persons : `list` -Shows a list of all persons in the address book. +Examples: +* `add n/John Doe p/98765432 e/johnd@example.com` +* `add n/Betsy Crowe e/betsycrowe@example.com p/1234567 c/CS2103T;CS2101` +* `add n/Nakahara Chuuya p/91199119 e/chuuya@gmail.com c/LAJ2201;CS2103T;CS2101` +![addExample.png](images/addExample.png) +### Listing all students : `liststudents` -Format: `list` +Shows a list of all students in TAHub. -### Editing a person : `edit` +Format: `liststudents` +![liststudentsExample.png](images/liststudentsExample.png) -Edits an existing person in the address book. +### Editing a student : `edit` -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Edits an existing student in TAHub. -* 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, …​ +Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [c/COURSE]…​` + +* Edits the student at the specified `INDEX`. The index refers to the index number shown in the displayed student list. The index **must be a positive integer** 1, 2, 3, …​ * At least one of the optional fields must be provided. * Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +* When editing courses, the existing courses of the student will be removed i.e adding of courses is not cumulative. +* You can remove all the student’s courses by typing `c/` without + specifying any courses 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. +* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email of the 1st student to be `91234567` and `johndoe@example.com` respectively. +* `edit 2 n/Betsy Crower c/` Edits the name of the 2nd student to be `Betsy Crower` and clears all existing courses. +* `edit 3 c/CS2103T;CS2101` Edits the courses of the 3rd student to be CS2103T & CS2101. +* `edit 4 p/91918181` +![editExample.png](images/editExample.png) -### Locating persons by name: `find` +### Locating students by name and/or course: `find` -Finds persons whose names contain any of the given keywords. +Finds students whose names contain any of the given keywords. Format: `find KEYWORD [MORE_KEYWORDS]` -* The search is case-insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* 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` +* The search is case-insensitive. e.g `hans` will match `Hans`, `cs2103t` will match `CS2103T` +* Can search for names and courses. Use the `n/` prefix to search for names and the `c/` prefix to search for courses. +* Partial searches will be matched e.g. `Jam` will match `James` and `James Ho` +* Each sequence of words not separated by `;` or a prefix will be used as a search. This means that `jam ho` will not match `James Ho` +* If a semicolon was used to separate searches, students matching at least one keyword will be returned (i.e. `OR` search). +* If a prefix was used to separate searches, students matching all keywords will be returned (i.e. `AND` search). +* If no names are provided to the find command (i.e. `find n/`), all students will be listed. +* **Warning**: `find c/` will not be treated as an error and will return 0 students. Refer to the [Proposed Features](#proposed features) below for details of the proposed changes to this command. Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +* `find n/John` returns `john` and `John Doe` +* `find n/alex;david` returns `Alex Yeoh`, `David Li`
+* `find n/alex n/david` returns `Alex David`, if a student with that name exists +* `find c/CS2103T c/CS2100` will return students who are taking both `CS2103T` and `CS2100` +* `find n/alex c/cs2103t;cs2100` will return all students whose names contain `alex` and are taking at least one of `CS2103T` or `CS2101`. +* `find n/` will return all students. +* `find c/` will return no students. +* `find n/Osamu;Chuuya` +![findNameExample.png](images/findNameExample.png) +* `find c/CS2103T` +![findCourseExample.png](images/findCourseExample.png) + +### Deleting a student : `delete` + +Deletes the specified student from TAHub. + +Format: `delete INDEX[;INDEX]…` + +* Deletes the student at the specified `INDEX`. +* The index refers to the index number shown in the displayed student list. +* The index **must be a positive integer** 1, 2, 3, …​ -### Deleting a person : `delete` +* Can delete multiple students at once by separating indices with semicolons (;). -Deletes the specified person from the address book. +Examples: +* `liststudents` followed by `delete 2` deletes the 2nd student in TAHub. +* `liststudents` followed by `delete 2;3` deletes the 2nd and 3rd student in TAHub. +* `find n/Betsy` followed by `delete 1` deletes the 1st student in the results of the `find` command. +* `liststudents` followed by `delete 4` deletes the 4th student in TAHub. +![deleteExample.png](images/deleteExample.png) -Format: `delete INDEX` +### Exporting student data : `export` -* 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, …​ +Exports the current list of students to a CSV file. + +Format: `export [-f] FILENAME` + +* Exports student data to 'FILENAME.csv' in both the data directory and user's home directory +* The `-f` flag is optional and allows overwriting of existing files +* The filename must contain only alphanumeric characters (A-Z, a-z, 0-9) +* Files are saved in both the data directory and home directory +* If the home directory copy fails, only the data directory file is created 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. +* `export students` creates students.csv containing current student list +* `export -f backup` overwrites backup.csv if it exists -### Clearing all entries : `clear` +### Importing student data : `import` -Clears all entries from the address book. +Imports students from a CSV file into TAHub. -Format: `clear` +Format: `import FILENAME` -### Exiting the program : `exit` +* The CSV file must have the header: Name,Phone,Email,Courses +* Files can be read from: + * Data directory (./data/): `import students.csv` + * Current directory: `import ./students.csv` + * Home directory (using ~ prefix): `import ~/documents/students.csv` + * Absolute paths: `import /path/to/students.csv` +* Students with validation errors will be logged in error.csv +* Duplicate students are skipped and logged +* Courses should be semicolon-separated in the CSV -Exits the program. +Examples: +* `import students.csv` imports student data from students.csv +* `import ~/documents/students.csv` imports from the home directory -Format: `exit` +# Consultations + +The consultation list is shown in the middle of TAHub.
+Currently, you can: +- Add and delete consultations +- Add and remove students from consultations +- Export and Import Consultation Data + +In a consultation, students are represented by name tags.
+ +![UgConsultation.png](images/UgConsultation.png) + +This is an example of a consultation. In this example, `Nakahara Chuuya` & `Osamu Dazai` +are in the consultation on 2024-12-05, 09:00. + +Additionally, consultations that have passed (the time is before your computer's time) +will be displayed in red, as follows: + +![UgRedConsultation.png](images/UgRedConsultation.png) + +## Consultation Commands + +### Adding a consultation : `addconsult` + +Adds a new consultation to TAHub. + +**Format**: `addconsult d/DATE t/TIME` + +* The date and time should not conflict with any existing consultation. +* Date format: `YYYY-MM-DD` +* Time format: `HH:mm` + +**Examples**: +* `addconsult d/2024-11-05 t/09:00` adds a Consultation Timing that has passed. +![addconsultExamplePast.png](images/addconsultExamplePast.png) +* `addconsult d/3345-01-11 t/01:00` adds a Consultation Timing. +![addconsultExample.png](images/addconsultExample.png) + +### Refreshing the consultation list : `listconsults` + +Refreshes and displays the consultation list. +Useful to fix minor UI glitches, e.g. the display not updating after adding a student. + +**Format**: `listconsults` + +**Example**: +* `listconsults` + +### Adding students to a consultation : `addtoconsult` + +Adds students to an existing consultation, specified by its index. + +**Format**: `addtoconsult INDEX [n/NAME]… [i/INDEX]…` + +* `INDEX` specifies the consultation to add students to. +* Student names (`n/NAME`) and/or student indices (`i/INDEX`) can be used to specify students. At least one name or +index must be provided. +* Students already in the consultation will not be added again, and an error message will be shown. +* Student names are **case-sensitive**. + +**Examples**: +* `addtoconsult 1 n/John Doe n/Harry Ng` +* `addtoconsult 2 i/3 i/5` adds students at indices 3 and 5 in the student list to the 2nd consultation. +* `addtoconsult 3 n/Nakahara Chuuya i/3` adds students Nakahara Chuuya & Student at Index 3 to the 3rd consultation. +![addtoconsultExample.png](images/addtoconsultExample.png) + +### Removing students from a consultation : `removefromconsult` + +Removes specified students from a consultation, identified by its index. + +**Format**: `removefromconsult INDEX n/NAME [n/NAME]…` + +* `INDEX` is the index of the consultation from which the students will be removed. +* Specify one or more students to remove by their names. +* Student names are **case-sensitive**. + +**Examples**: +* `removefromconsult 1 n/John Doe n/Harry Ng` removes students named John Doe and Harry Ng from the 1st consultation. +* `removefromconsult 3 n/Osamu Dazai` removes student named Osamu Dazai from the 3rd consultation. +![removefromconsultExample.png](images/removefromconsultExample.png) + +### Deleting consultations : `deleteconsult` + +Deletes one or more consultations from TAHub by their indices. + +**Format**: `deleteconsult INDEX[;INDEX]…` + +* `INDEX` specifies the consultation to delete. You can delete multiple consultations by separating indices with semicolons (`;`). + +**Examples**: +* `deleteconsult 2` +![deleteconsultExample.png](images/deleteconsultExample.png) +* `deleteconsult 1;3;5` (deletes the 1st, 3rd, and 5th consultations) + +### Exporting consultation data : `exportconsult` + +Exports the current list of consultations to a CSV file. + +Format: `exportconsult [-f] FILENAME` + +* Exports consultation data to 'FILENAME.csv' in both the data directory and user's home directory +* The `-f` flag is optional and allows overwriting of existing files +* The filename must contain only alphanumeric characters (A-Z, a-z, 0-9) +* Files are saved in both the data directory and home directory +* If the home directory copy fails, only the data directory file is created + +Examples: +* `exportconsult sessions` creates sessions.csv containing current consultation list +* `exportconsult -f consultbackup` overwrites consultbackup.csv if it exists + +### Importing consultation data : `importconsult` + +Imports consultations from a CSV file into TAHub. + +Format: `importconsult FILENAME` + +* The CSV file must have the header: Date,Time,Students +* Files can be read from: + * Data directory (./data/): `importconsult sessions.csv` + * Current directory: `importconsult ./sessions.csv` + * Home directory (using ~ prefix): `importconsult ~/documents/sessions.csv` + * Absolute paths: `importconsult /path/to/sessions.csv` +* Date must be in YYYY-MM-DD format +* Time must be in HH:mm format (24-hour) +* Students must be semicolon-separated and exist in TAHub +* Consultations with validation errors will be logged in error.csv +* Duplicate consultations are skipped and logged + +Examples: +* `importconsult sessions.csv` imports consultation data from sessions.csv +* `importconsult ~/documents/consultations.csv` imports from the home directory + +# Lessons + +The lesson list is shown on the right side of TAHub.
+Currently, you can: +- Add and delete lessons +- Add and remove students from lessons +- Mark students' attendance and participation + +In a lesson, students are represented by name tags.
+Its color represents their attendance (green for present, red for absent)
+The number next to a student's name represents their participation score. + +![UgLesson.png](images/UgLesson.png) + +This is an example of a lesson. In this example, `Jane Doe` is absent, +and has a participation of `0`. `John Doe` is present with a participation +of `2`. + +Additionally, lessons that have passed (the time is before your computer's time) +will be displayed in red, as follows: + +![UgRedLesson.png](images/UgRedLesson.png) + +## Lesson Commands + +### Adding a Lesson : `addlesson` + +Adds a lesson to TAHub. Lessons will be sorted in chronological order +in the lesson list. + +Format: `addlesson d/DATE t/TIME` + +* `DATE` must be in the format `YYYY-MM-DD`, and must be a valid date. +* `TIME` must be in the format `HH:mm`, and must be a valid time. + +**Examples**: +* `addlesson d/2024-11-08 t/08:00` adds a Lesson Timing that has passed. + ![addlessonExamplePast.png](images/addlessonExamplePast.png) +* `addlesson d/3145-11-23 t/01:00` adds a Lesson Timing. + ![addlessonExample.png](images/addlessonExample.png) + +### Refreshing the lesson list : `listlessons` + +Refreshes and displays the lesson list. +Useful to fix minor UI glitches, e.g. the display not updating after adding a student. + +**Format**: `listlessons` + +**Example**: +* `listlessons` + +### Adding a student to a lesson : `addtolesson` + +Adds student(s) to a lesson. Students added to a lesson will be shown as name tags under +that lesson inside the lesson list. + +Format: `addtolesson LESSON_INDEX [n/NAME]… [i/STUDENT_INDEX]…` + +* `LESSON_INDEX` is the index of the lesson as displayed in the lesson list. +* At least one of the optional arguments must be provided. There must be at least one name or index. +* `NAME` must be the full name of a student exactly as shown in the student list. Names are **case-sensitive**. +* `STUDENT_INDEX` is the index of a student as displayed in the student list. + +Examples: +* `addtolesson 1 n/John Doe` adds `John Doe` to lesson number 1. +* `addtolesson 1 n/John Doe i/3 i/5` adds `John Doe` and students numbered 3 and 5 to lesson number 1. +* `addtolesson 3 n/Nakahara Chuuya i/3` adds students Nakahara Chuuya & Student at Index 3 to the 3rd lesson. + ![addtolessonExample.png](images/addtolessonExample.png) + +### Removing a student from a lesson : `removefromlesson` + +Removes student(s) from a lesson. Removing a student will also remove all data associated +with them to that lesson, i.e. re-adding them defaults to no attendance and 0 participation. + +Format: `removefromlesson LESSON_INDEX n/NAME [n/NAME]…` + +* `LESSON_INDEX` is the index of the lesson as displayed in the lesson list. +* `NAME` must be the full name of a student in the lesson. Names are **case-sensitive**. + +Examples: +* `removefromlesson 1 n/John Doe n/Jane Doe` removes `John Doe` and `Jane Doe` from lesson number 1. +* `removefromlesson 3 n/Osamu Dazai` removes student named Osamu Dazai from the 3rd lesson. +![removefromlessonExample.png](images/removefromlessonExample.png) +* +### Marking a student's attendance : `marka` + +Marks student(s)' attendance in a lesson. The student's attendance is represented by the +color of their name tag under a lesson - **green** for present and **red** for absent. + +Format: `marka LESSON_INDEX n/NAME [n/NAME]… a/ATTENDANCE` + +* `LESSON_INDEX` is the index of the lesson as displayed in the lesson list. +* `NAME` must be the full name of a student in the lesson. Names are **case-sensitive**. +* If multiple names are provided, all their attendances will be set to the given value. +* `ATTENDANCE` must be one of the following: `Y`,`y`or`1` for yes (student is present) and `N`,`n`or`0` for no (student is absent). +* There must be exactly 1 `ATTENDANCE` argument, e.g. `a/1 a/1` is not allowed. + +Examples: +* `marka 1 n/John Doe a/y` marks `John Doe` as present for lesson number 1. +* `marka 2 n/John Doe n/Jane Doe a/N` marks `John Doe` and `Jane Doe` as absent for lesson number 2. +* `marka 3 n/Nakahara Chuuya a/Y` marks Student Nakahara Chuuya as present for lesson number 3. +![markaExample.png](images/markaExample.png) + +### Marking a student's participation : `markp` + +Marks student(s)' participation in a lesson. The student's participation is reflected in the number next to +their name tag under a lesson.
+**Additionally, setting a student's participation above 0 +will also automatically set their attendance to true.** + +Format: `markp LESSON_INDEX n/NAME [n/NAME]… pt/PARTICIPATION` + +* `LESSON_INDEX` is the index of the lesson as displayed in the lesson list. +* `NAME` must be the full name of a student in the lesson. Names are **case-sensitive**. +* `LESSON_INDEX` is the index of the lesson as displayed in the lesson list. +* `NAME` must be the full name of a student in the lesson. +* If multiple names are provided, all their participation points will be set to the given value. +* `PARTICIPATION` must be an integer between 0 and 100 inclusive. +* There must be exactly 1 `PARTICIPATION` argument, e.g. `pt/3 pt/3` is not allowed. +* The participation score is set **exactly** to the given value. It does not add onto students' existing score. +* Due to a bug in the current version, a `+` before the participation value will be accepted, i.e. `+3` is treated as +`3`. This will be removed in a future release. + +Examples: +* `markp 1 n/John Doe pt/3` marks `John Doe` as having 3 participation marks for lesson number 1. +* `markp 2 n/John Doe n/Jane Doe pt/5` marks `John Doe` and `Jane Doe` as having 5 participation marks for lesson number 2. +* `markp 3 n/Nakahara Chuuya pt/100` marks Student Nakahara Chuuya as having 100 participation marks for lesson number 3. + ![markpExample.png](images/markpExample.png) + +### Deleting a lesson : `deletelesson` + +Deletes lesson(s) from TAHub. + +Format: `deletelesson LESSON_INDEX[;LESSON_INDEX]…` + +* `LESSON_INDEX` is the index of the lesson as displayed in the lesson list. + +Examples: +* `deletelesson 2` +![deletelessonExample.png](images/deletelessonExample.png) +* `deletelesson 1;2;3` deletes the lessons numbered 1,2,3 in the lesson list + +## Storage Operations ### 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. +TAHub data is saved in the hard disk automatically after any command that changes the data. There is no need to save manually. ### Editing the data file -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +TAHub data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file.
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
-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, TAHub 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 TAHub 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]` @@ -174,8 +576,36 @@ _Details coming soon ..._ ## FAQ +**Q**: The Command I entered does not work. What's Wrong?
+**A**: Check the Instructions for the Command Format in [Features](#features). Refer to the Examples provided as necessary.
+ +**Q**: Can I run TAHub on a Windows, Mac or Linux Computer?
+**A**: Yes. If your Computer has Java 17 or above installed, TAHub will work on Windows, Mac, and Linux Computers.
+ +**Q**: Will editing / deleting Students in the Student List change the details of existing students in the Consultation / Lesson List?
+**A**: Yes. Any changes made to Students will be reflected immediately in the Consultation & Lesson List.
+ +**Q**: Can I Import / Export Lesson Data in TAHub?
+**A**: No. The Feature for Importing/Exporting Lesson Data has not been implemented in the current version. Our Team has plans to implement this feature in next version.
+ +**Q**: Does TAHub require Internet Connection?
+**A**: No. After installation, TAHub does not require internet connection. + +**Q**: Where is the data for TAHub stored?
+**A**: TAHub data are saved automatically as a JSON file in `[JAR file location]/data/addressbook.json`. You can make a backup of the file if you wish to.
+ **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 TAHub home folder.
+ +**Q**: Do I need to manually save the data?
+**A**: TAHub saves automatically after any command that modifies the data. There is no need to save manually.
+ +**Q**: Can I edit the JSON Data File directly
+**A**: You are strongly not encouraged to edit the JSON data file directly. +You can use the commands as mentioned in the [Features](#features) to augment any data. +Should the changes made to the data file causes the format to be invalid, TAHub will discard all data and start with an empty data file. +It is highly recommended to make a copy of the date file before editing it. +However, Advanced users are welcome to update data directly by editing that data file.
-------------------------------------------------------------------------------------------------------------------- @@ -183,6 +613,8 @@ _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. Occasionally, **the consults and lessons list** may not display properly, i.e. have wrong heights. Try using the relevant list command `listconsults` or `listlessons` to refresh the list. Alternatively, try scrolling the list with a mouse. This should update the display to remedy the bug. +4. Certain characters cannot be used in student names, such as `/` or `-`, which prevent legitimate parts of names such as `S/O` from being input. This will be addressed in a future version. -------------------------------------------------------------------------------------------------------------------- @@ -190,10 +622,26 @@ _Details coming soon ..._ Action | Format, Examples --------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` +**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL [c/COURSE]…​`
e.g., `add n/James Ho p/98765432 e/jamesho@example.com c/CS2103T;CS2101` **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` +**Delete** | `delete INDEX [;INDEX]…​`
e.g., `delete 3`
e.g., `delete 2;3;4` +**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [c/COURSE]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com c/CS2100`
e.g.,`edit 2 c/CS2103T;CS2101;CS2106`
e.g.,`edit 2 c/CS2103T c/CS2101 c/CS2106`
e.g.,`edit 2 c/CS2103T;CS2101 c/CS2106` +**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find n/James Jake` to find all students named `James Jake`
e.g.,`find n/James;Jake` to find all students whose names contain either `James` or `Jake`
e.g.,`find c/CS2103T c/CS2100` to find all students who are enrolled both `CS2103T` and `CS2100`
e.g.,`find n/James c/CS2103T` to find all students whose names contain `James` and are enrolled in `CS2103T` +**List Students** | `liststudents` **Help** | `help` +**Export Students** | `export [-f] FILENAME`
e.g., `export students` (relative)
e.g., `export ~/Documents/students` (home) +**Export Consultations** | `exportconsult [-f] FILENAME`
e.g., `exportconsult sessions` (relative)
e.g., `exportconsult ~/Documents/sessions` (home) +**Import Students** | `import FILENAME`
e.g., `import students.csv` (relative)
e.g., `import ~/Documents/students.csv` (home) +**Import Consultations** | `importconsult FILENAME`
e.g., `importconsult sessions.csv` (relative)
e.g., `importconsult ~/Documents/sessions.csv` (home) +**Add Consultation** | `addconsult d/DATE t/TIME`
e.g., `addconsult d/2024-10-20 t/14:00` +**Add to Consultation** | `addtoconsult INDEX [n/NAME]…​ [i/STUDENT_INDEX]…​`
e.g., `addtoconsult 1 n/James Jake n/John Jill i/2 i/3` +**Remove from Consultation** | `removefromconsult INDEX n/NAME…​`
e.g., `removefromconsult 1 n/Jake John`
e.g., `removefromconsult 1 n/Jake n/John` +**Delete Consultations** | `deleteconsult INDEX [;INDEX]…​`
e.g., `deleteconsult 3`
e.g., `deleteconsult 2;3;4` +**List Consultations** | `listconsults` +**Add Lesson** | `addlesson d/DATE t/TIME`
e.g., `addlesson d/2024-10-20 t/14:00` +**Add to Lesson** | `addtolesson INDEX [n/NAME]…​ [i/STUDENT_INDEX]…​`
e.g., `addtolesson 1 n/James Jake n/John Jill i/2 i/3` +**Remove from Lesson** | `removefromlesson INDEX n/NAME…​`
e.g., `removefromlesson 1 n/Jake John`
e.g., `removefromlesson 1 n/Jake n/John` +**Delete Lesson** | `deletelesson INDEX [;INDEX]…​`
e.g., `deletelesson 3`
e.g., `deletelesson 2;3;4` +**List Lessons** | `listlessons` +**Mark Attendance for Lesson** | `marka INDEX n/NAME…​ a/ATTENDANCE`
e.g., `marka 3 n/Jack a/y`
e.g., `marka 3 n/Jack n/Jill a/1`
e.g., `marka 3 n/Jack a/n`
e.g., `marka 3 n/Jack a/0` +**Mark Participation for Lesson** | `markp INDEX n/NAME…​ pt/POINTS`
e.g., `markp 3 n/Jack pt/75` diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..f6bf7163054 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "TAHub" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2425S1-CS2103T-F13-1/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..649774fa729 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: "TAHub"; font-size: 32px; } } diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index 48b6cc4333c..1a887d8c1d4 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -14,7 +14,7 @@ activate ui UI_COLOR ui -[UI_COLOR]> logic : execute("delete 1") activate logic LOGIC_COLOR -logic -[LOGIC_COLOR]> model : deletePerson(p) +logic -[LOGIC_COLOR]> model : deleteStudent(p) activate model MODEL_COLOR model -[MODEL_COLOR]-> logic diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..796f979e1e5 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -4,18 +4,17 @@ skinparam arrowThickness 1.1 skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR -AddressBook *-right-> "1" UniquePersonList -AddressBook *-right-> "1" UniqueTagList -UniqueTagList -[hidden]down- UniquePersonList -UniqueTagList -[hidden]down- UniquePersonList +AddressBook *-right-> "1" UniqueStudentList +AddressBook *-right-> "1" UniqueCourseList +UniqueCourseList -[hidden]down- UniqueStudentList +UniqueCourseList -[hidden]down- UniqueStudentList -UniqueTagList -right-> "*" Tag -UniquePersonList -right-> Person +UniqueCourseList -right-> "*" Course +UniqueStudentList -right-> Student -Person -up-> "*" Tag +Student -up-> "*" Course -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address +Student *--> Name +Student *--> Phone +Student *--> Email @enduml diff --git a/docs/diagrams/Consults/ConsultCommands.puml b/docs/diagrams/Consults/ConsultCommands.puml new file mode 100644 index 00000000000..3902b1c63db --- /dev/null +++ b/docs/diagrams/Consults/ConsultCommands.puml @@ -0,0 +1,45 @@ +@startuml + +!include ../style.puml + +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +title Add Consult Command and Parser UML Diagram +package Logic { + + package Commands { + Class "{abstract}\nCommand" as Command + Class AddConsultCommand + Class CommandResult + } + + package "Parser Classes" { + Class AddConsultCommandParser + } +} + +package Model { + Class Consultation + Class Student + Class Date + Class Time +} + +' Command relationships +Command <|-- AddConsultCommand +Command ..> CommandResult : creates > + +' Parser relationships +AddConsultCommandParser ..> AddConsultCommand : creates > + +' Model relationships +Consultation *-- "*" Student +Consultation *-- "1" Date +Consultation *-- "1" Time + +' Group visibility for logical structure +CommandResult ..> Consultation : creates > + +@enduml diff --git a/docs/diagrams/Consults/SequenceDiagramAddToConsultCommand.puml b/docs/diagrams/Consults/SequenceDiagramAddToConsultCommand.puml new file mode 100644 index 00000000000..f3654f70fbe --- /dev/null +++ b/docs/diagrams/Consults/SequenceDiagramAddToConsultCommand.puml @@ -0,0 +1,84 @@ + +@startuml +!include ../style.puml +skinparam ArrowFontStyle plain + +title AddToConsultCommand Sequence Diagram + +box Logic +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as addressBookParser LOGIC_COLOR_T3 +participant ":AddToConsultCommandParser" as AddToConsultCommandParser LOGIC_COLOR_T2 +participant ":AddToConsultCommand" as AddToConsultCommand LOGIC_COLOR_T1 +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +Participant ":Model" as model MODEL_COLOR +end box + +-[UI_COLOR]> LogicManager : execute("addtoconsult 1 n/John Doe i/3") +activate LogicManager LOGIC_COLOR + +'Logic manager calls AddressBookParser and passes the command +LogicManager -[LOGIC_COLOR]> addressBookParser : parseCommand("addtoconsult 1 n/John Doe i/3") +activate addressBookParser LOGIC_COLOR_T3 + +'AddressBookParser creates an instance of the parser +create AddToConsultCommandParser +addressBookParser -[LOGIC_COLOR_T3]> AddToConsultCommandParser : +activate AddToConsultCommandParser LOGIC_COLOR_T2 + +AddToConsultCommandParser --[LOGIC_COLOR_T2]> addressBookParser +deactivate AddToConsultCommandParser + +' AddressBookParser calls the parse command +addressBookParser -[LOGIC_COLOR_T3]> AddToConsultCommandParser : parse("1 n/John Doe i/3") +activate AddToConsultCommandParser LOGIC_COLOR_T2 + +'Within the parse command, the parser creates an instance of the command +create AddToConsultCommand +AddToConsultCommandParser --[LOGIC_COLOR_T2]> AddToConsultCommand : +activate AddToConsultCommand LOGIC_COLOR_T1 + +AddToConsultCommand --[LOGIC_COLOR]> AddToConsultCommandParser +deactivate AddToConsultCommand LOGIC_COLOR_T1 + +'parser returns the command to the AddressBookParser +AddToConsultCommandParser --[LOGIC_COLOR_T2]> addressBookParser : AddToConsultCommand +deactivate AddToConsultCommandParser + +AddToConsultCommandParser --[hidden]> addressBookParser : AddToConsultCommand +destroy AddToConsultCommandParser + +addressBookParser --[LOGIC_COLOR_T3]> LogicManager : AddToConsultCommand +deactivate addressBookParser + +'LogicManager calls command.execute +LogicManager -[LOGIC_COLOR]> AddToConsultCommand : execute(model) +activate AddToConsultCommand LOGIC_COLOR_T1 + +'get filteredConsultationList +AddToConsultCommand -[LOGIC_COLOR]> model : addStudent(student) +activate model MODEL_COLOR + + +model -[MODEL_COLOR]-> AddToConsultCommand +deactivate model + +create CommandResult +AddToConsultCommand --[LOGIC_COLOR_T2]> CommandResult +activate CommandResult LOGIC_COLOR +CommandResult --[LOGIC_COLOR]> AddToConsultCommand +deactivate CommandResult + + +'After adding students, return new consultation +AddToConsultCommand --[LOGIC_COLOR]> LogicManager : CommandResult +deactivate AddToConsultCommand LOGIC_COLOR_T1 +AddToConsultCommand --[hidden]> LogicManager : CommandResult +destroy AddToConsultCommand + +[<-[UI_COLOR]-LogicManager : CommandResult + +@enduml diff --git a/docs/diagrams/Consults/addStudent_for_consult.puml b/docs/diagrams/Consults/addStudent_for_consult.puml new file mode 100644 index 00000000000..f9f7e25caf9 --- /dev/null +++ b/docs/diagrams/Consults/addStudent_for_consult.puml @@ -0,0 +1,38 @@ +@startuml +!include ../style.puml +skinparam ArrowFontStyle plain + +title addStudent Method Sequence Diagram + +box Logic +participant ":AddToConsultCommand" as client LOGIC_COLOR +end box + +box Model +participant ":Consultation" as consultation MODEL_COLOR +end box + + + +client -> consultation : addStudent(student) +activate consultation + + consultation -> consultation : hasStudent(student) + activate consultation + + consultation -> consultation : + deactivate consultation + +alt student already in consult + + consultation -> client : nothing is added, exception thrown + +else student not in consult + + consultation -> client : student added to consult successfully + +end alt + +deactivate consultation + +@enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 5241e79d7da..e7a1e82a9fd 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -49,7 +49,7 @@ deactivate AddressBookParser LogicManager -> DeleteCommand : execute(m) activate DeleteCommand -DeleteCommand -> Model : deletePerson(1) +DeleteCommand -> Model : deleteStudent(1) activate Model Model --> DeleteCommand diff --git a/docs/diagrams/FullerLogicClassDiagram.puml b/docs/diagrams/FullerLogicClassDiagram.puml new file mode 100644 index 00000000000..3af41c045be --- /dev/null +++ b/docs/diagrams/FullerLogicClassDiagram.puml @@ -0,0 +1,71 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +package Logic { + Class "<>\nLogic" as LogicClass + Class LogicManager + + package Commands { + Class "{abstract}\nCommand" as Command + Class CommandResult + Class XYZCommand + note right of XYZCommand: XYZCommand = AddCommand, \nFindCommand, etc + } + + package "Parser Classes" { + Class "<>\nParser" as Parser + Class AddressBookParser + Class XYZCommandParser + Class ArgumentMultimap + Class ArgumentTokenizer + Class ParserUtil + Class Prefix + Class CliSyntax + } +} + +package Model { + Class "<>\nModel" as ModelClass + Class Student +} + +package Storage { + Class "<>\nStorage" as StorageClass +} + +' Logic relationships +LogicManager .up.|> LogicClass +LogicManager -right-> "1" AddressBookParser +LogicManager .down.> CommandResult +LogicManager .> Command : executes > +LogicManager --> ModelClass +LogicManager --> StorageClass + +' Command relationships +Command <|-- XYZCommand +Command .up.> CommandResult : creates > +Command .right.> ModelClass : uses > + +' Parser relationships +Parser <|.. XYZCommandParser +AddressBookParser .down.> XYZCommandParser : creates > +XYZCommandParser ..> ArgumentMultimap +XYZCommandParser ..> ArgumentTokenizer +XYZCommandParser ..> ParserUtil +XYZCommandParser ..> XYZCommand : creates > +ArgumentTokenizer .> ArgumentMultimap +CliSyntax ..> Prefix +ParserUtil .> Prefix +ArgumentTokenizer .> Prefix + +' Cross-package relationships +LogicClass ..> CommandResult +Command ..> Student : manipulates > +ModelClass ..> Student : manages > + +' Hidden relationships for layout +Storage -[hidden]- ModelClass +@enduml diff --git a/docs/diagrams/ImportExport.puml b/docs/diagrams/ImportExport.puml new file mode 100644 index 00000000000..8712ee1de86 --- /dev/null +++ b/docs/diagrams/ImportExport.puml @@ -0,0 +1,37 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +title Import Command and Parser UML Diagram +package Logic { + package Commands { + Class "{abstract}\nCommand" as Command + Class ImportCommand + Class CommandResult + } + + package Parsers { + Class ImportCommandParser + } +} + +package Model { + Class Course + Class Student +} + +' Command relationships +Command <|-- ImportCommand +Command ..> CommandResult : produces > + +ImportCommandParser ..> ImportCommand : creates > + +' Model relationships +ImportCommand ..> Student : creates > +ImportCommand ..> Course : creates > + +' Group visibility for logical structure +ImportCommand ..> CommandResult +@enduml diff --git a/docs/diagrams/Lessons/LessonsAndRelatedCommands.puml b/docs/diagrams/Lessons/LessonsAndRelatedCommands.puml new file mode 100644 index 00000000000..9383890eb50 --- /dev/null +++ b/docs/diagrams/Lessons/LessonsAndRelatedCommands.puml @@ -0,0 +1,52 @@ +@startuml + +!include ../style.puml + +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +title Add Lesson Command and Parser UML Diagram +package Logic { + + package Commands { + Class "{abstract}\nCommand" as Command + Class AddLessonCommand + Class CommandResult + } + + package "Parser Classes" { + Class AddLessonCommandParser + } +} + +package Model { + Class Lesson + Class StudentLessonInfo + Class Student + Class Date + Class Time + Class Attendance + Class ParticipationScore +} + +' Command relationships +Command <|-- AddLessonCommand +Command ..> CommandResult : creates > + +' Parser relationships +AddLessonCommandParser ..> AddLessonCommand : creates > + +' Model relationships +Lesson *-- "*" StudentLessonInfo +StudentLessonInfo --> "1" Student : has > +Lesson *-- "1" Date +Lesson *-- "1" Time + +' Group visibility for logical structure +CommandResult ..> Lesson : creates > + +StudentLessonInfo *-- "1" Attendance : records > +StudentLessonInfo *-- "1" ParticipationScore : tracks > + +@enduml diff --git a/docs/diagrams/Lessons/MarkAttendanceCommandSequenceDiagram.puml b/docs/diagrams/Lessons/MarkAttendanceCommandSequenceDiagram.puml new file mode 100644 index 00000000000..4be6fbd029e --- /dev/null +++ b/docs/diagrams/Lessons/MarkAttendanceCommandSequenceDiagram.puml @@ -0,0 +1,55 @@ +@startuml +!include ../style.puml +skinparam ArrowFontStyle plain + +title MarkLessonAttendanceCommand Sequence Diagram + +box Logic LOGIC_COLOR_T1 +participant ":MarkLessonAttendanceCommand" as command LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as model MODEL_COLOR +participant ":Lesson" as lesson MODEL_COLOR_T2 +end box + +[-> command : execute(model) +activate command LOGIC_COLOR +command -[MODEL_COLOR]> model : getFilteredLessonList() +activate model MODEL_COLOR + +model --[MODEL_COLOR]> command : list of lessons +deactivate model + +create lesson +command -[LOGIC_COLOR]> lesson : new Lesson(targetLesson) +activate lesson MODEL_COLOR_T2 + +lesson --[LOGIC_COLOR]> command +deactivate lesson MODEL_COLOR_T2 + +box ModelLoop +loop for each student + command -[MODEL_COLOR]> model : findStudentByName(studentName) + activate model MODEL_COLOR + + model --[MODEL_COLOR]> command : student + deactivate model + + command -[LOGIC_COLOR]> lesson : setAttendance(student, attendance) + activate lesson MODEL_COLOR_T2 + lesson --[LOGIC_COLOR]> command : + deactivate lesson +end +end box + +command -[MODEL_COLOR]> model : setLesson(targetLesson, newLesson) +activate model MODEL_COLOR + +model --[MODEL_COLOR]> command +deactivate model + +[<--command : CommandResult +deactivate command LOGIC_COLOR + +@enduml diff --git a/docs/diagrams/Lessons/SequenceDiagramAddToLessonCommand.puml b/docs/diagrams/Lessons/SequenceDiagramAddToLessonCommand.puml new file mode 100644 index 00000000000..a7ec912c37d --- /dev/null +++ b/docs/diagrams/Lessons/SequenceDiagramAddToLessonCommand.puml @@ -0,0 +1,82 @@ +@startuml +!include ../style.puml +skinparam ArrowFontStyle plain + +title AddToLessonCommand Sequence Diagram + +box Logic +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as addressBookParser LOGIC_COLOR_T3 +participant ":AddToLessonCommandParser" as AddToLessonCommandParser LOGIC_COLOR_T2 +participant ":AddToLessonCommand" as AddToLessonCommand LOGIC_COLOR_T1 +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +Participant ":Model" as model MODEL_COLOR +end box + +-[UI_COLOR]> LogicManager : execute("addtolesson 1 n/John Doe i/3") +activate LogicManager LOGIC_COLOR + +'Logic manager calls AddressBookParser and passes the command +LogicManager -[LOGIC_COLOR]> addressBookParser : parseCommand("addtolesson 1 n/John Doe i/3") +activate addressBookParser LOGIC_COLOR_T3 + +'AddressBookParser creates an instance of the parser +create AddToLessonCommandParser +addressBookParser -[LOGIC_COLOR_T3]> AddToLessonCommandParser +activate AddToLessonCommandParser LOGIC_COLOR_T2 + +AddToLessonCommandParser --[LOGIC_COLOR_T2]> addressBookParser +deactivate AddToLessonCommandParser + +' AddressBookParser calls the parse command +addressBookParser -[LOGIC_COLOR_T3]> AddToLessonCommandParser : parse("addtolesson 1 n/John Doe i/3") +activate AddToLessonCommandParser LOGIC_COLOR_T2 + +'Within the parse command, the parser creates an instance of the command +create AddToLessonCommand +AddToLessonCommandParser --[LOGIC_COLOR_T2]> AddToLessonCommand +activate AddToLessonCommand LOGIC_COLOR_T1 + +AddToLessonCommand --[LOGIC_COLOR]> AddToLessonCommandParser +deactivate AddToLessonCommand LOGIC_COLOR_T1 + +'parser returns the command to the AddressBookParser +AddToLessonCommandParser --[LOGIC_COLOR_T2]> addressBookParser : AddToLessonCommand +deactivate AddToLessonCommandParser + +AddToLessonCommandParser --[hidden]> addressBookParser : AddToLessonCommand +destroy AddToLessonCommandParser + +addressBookParser --[LOGIC_COLOR_T3]> LogicManager : AddToLessonCommand +deactivate addressBookParser + +'LogicManager calls command.execute +LogicManager -[LOGIC_COLOR]> AddToLessonCommand : execute(model) +activate AddToLessonCommand LOGIC_COLOR_T1 + +'get filteredStudentList +AddToLessonCommand -[LOGIC_COLOR]> model : addStudent +activate model MODEL_COLOR + +model -[MODEL_COLOR]-> AddToLessonCommand +deactivate model + +create CommandResult +AddToLessonCommand --[LOGIC_COLOR_T2]> CommandResult +activate CommandResult LOGIC_COLOR +CommandResult --[LOGIC_COLOR]> AddToLessonCommand +deactivate CommandResult + +'After adding students, return new lesson +AddToLessonCommand --[LOGIC_COLOR]> LogicManager : CommandResult +deactivate AddToLessonCommand LOGIC_COLOR_T1 +AddToLessonCommand --[hidden]> LogicManager : CommandResult +destroy AddToLessonCommand + +[<-[UI_COLOR]-LogicManager : CommandResult + + +@enduml diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index 58b4f602ce6..22fc1dee9e0 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -19,6 +19,8 @@ Class LogicManager package Model { Class HiddenModel #FFFFFF +Class Consultation +Class Student } package Storage { @@ -36,6 +38,9 @@ LogicManager .left.> Command : <> LogicManager --> Model LogicManager --> Storage +LogicManager --> Consultation : <> +LogicManager --> Student : <> + Storage --[hidden] Model Command .[hidden]up.> Storage Command .right.> Model diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..a04f14fe6f0 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -12,13 +12,23 @@ Class AddressBook Class ModelManager Class UserPrefs -Class UniquePersonList -Class Person -Class Address +Class UniqueStudentList +Class Student Class Email Class Name Class Phone -Class Tag +Class Course + +Class UniqueConsultList +Class Consultation +Class Date +Class Time + +Class UniqueLessonList +Class Lesson +Class StudentLessonInfo +Class Attendance +Class Participation Class I #FFFFFF } @@ -35,20 +45,35 @@ ModelManager -left-> "1" AddressBook ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList -UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag +AddressBook *--> "1" UniqueStudentList +UniqueStudentList --> "~* all" Student +Student *--> Name +Student *--> Phone +Student *--> Email +Student *--> "0..*" Course + +AddressBook *--> "1" UniqueConsultList +UniqueConsultList --> "~* all" Consultation +Consultation *--> Date +Consultation *--> Time +Consultation -down--> "0..*" Student + +AddressBook *--> "1" UniqueLessonList +UniqueLessonList --> "~* all" Lesson +Lesson *--> Date +Lesson *--> Time +Lesson -down--> "0..*" StudentLessonInfo -Person -[hidden]up--> I -UniquePersonList -[hidden]right-> I +StudentLessonInfo -left-> "1" Student +StudentLessonInfo -down-> Attendance +StudentLessonInfo -down-> Participation -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +Student -[hidden]up--> I +UniqueStudentList -[hidden]right-> I +UniqueConsultList -[hidden]left-> I +UniqueLessonList -[hidden]right-> I -ModelManager --> "~* filtered" Person +ModelManager -right--> "~* filtered" Student +ModelManager --> "~* filtered" Consultation +ModelManager -right--> "~* filtered" Lesson @enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..3930de3b6b7 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -18,8 +18,11 @@ package "AddressBook Storage" #F4F6F6{ Class "<>\nAddressBookStorage" as AddressBookStorage Class JsonAddressBookStorage Class JsonSerializableAddressBook -Class JsonAdaptedPerson -Class JsonAdaptedTag +Class JsonAdaptedStudent +Class JsonAdaptedConsultation +Class JsonAdaptedLesson +Class JsonAdaptedCourse +Class JsonAdaptedStudentLessonInfo } } @@ -37,7 +40,12 @@ Storage -right-|> AddressBookStorage JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook -JsonSerializableAddressBook --> "*" JsonAdaptedPerson -JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonSerializableAddressBook --> "*" JsonAdaptedStudent +JsonSerializableAddressBook --> "*" JsonAdaptedConsultation +JsonSerializableAddressBook --> "*" JsonAdaptedLesson +JsonAdaptedStudent --> "*" JsonAdaptedCourse +JsonAdaptedConsultation --> "*" JsonAdaptedStudent +JsonAdaptedLesson --> "*" JsonAdaptedStudentLessonInfo +JsonAdaptedStudentLessonInfo --> "1" JsonAdaptedStudent @enduml diff --git a/docs/diagrams/Student.puml b/docs/diagrams/Student.puml new file mode 100644 index 00000000000..ba80d05448d --- /dev/null +++ b/docs/diagrams/Student.puml @@ -0,0 +1,53 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR_T4 +skinparam classBackgroundColor MODEL_COLOR +skinparam classAttributeIconSize 0 + +show footbox +show members + +skinparam class { + attributeFontStyle underline + methodFontStyle underline +} + + +class Name { + - String fullName <> + + {static} isValidName() : boolean +} + +class Phone { + - String value <> + + {static} isValidPhone() : boolean +} + +class Email { + - String value <> + + {static} isValidEmail() : boolean +} + +class Course { + - String courseCode <> + + {static} isValidCourse() : boolean +} + +class Student { + - Name name <> + - Phone phone <> + - Email email <> + - Set courses <> + + getName(): Name + + getPhone(): Phone + + getEmail(): Email + + getCourse(): Set +} + +Student *-- Name +Student *-- Email +Student *-- Phone +Student --> "0..*" Course + +@enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..53c63d5cd0f 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -11,8 +11,12 @@ Class UiManager Class MainWindow Class HelpWindow Class ResultDisplay -Class PersonListPanel -Class PersonCard +Class StudentListPanel +Class StudentCard +Class ConsultationListPanel +Class ConsultationCard +Class LessonListPanel +Class LessonCard Class StatusBarFooter Class CommandBox } @@ -32,26 +36,38 @@ UiManager .left.|> Ui UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay -MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" StudentListPanel +MainWindow *-down-> "1" ConsultationListPanel +MainWindow *-down-> "1" LessonListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow -PersonListPanel -down-> "*" PersonCard +StudentListPanel -down-> "*" StudentCard +ConsultationListPanel -down-> "*" ConsultationCard +LessonListPanel -down-> "*" LessonCard MainWindow -left-|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart +StudentListPanel --|> UiPart +StudentCard --|> UiPart +ConsultationListPanel --|> UiPart +ConsultationCard --|> UiPart +LessonListPanel --|> UiPart +LessonCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart -PersonCard ..> Model +StudentCard ..> Model +ConsultationCard ..> Model +LessonCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic -PersonListPanel -[hidden]left- HelpWindow +StudentListPanel -[hidden]left- HelpWindow +ConsultationListPanel -[hidden]left- HelpWindow +LessonListPanel -[hidden]left- HelpWindow HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay ResultDisplay -[hidden]left- StatusBarFooter diff --git a/docs/diagrams/UndoRedoState2.puml b/docs/diagrams/UndoRedoState2.puml index ad32fce1b0b..3b5164d6bfc 100644 --- a/docs/diagrams/UndoRedoState2.puml +++ b/docs/diagrams/UndoRedoState2.puml @@ -4,7 +4,7 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 skinparam ClassBackgroundColor #FFFFAA -title After command "add n/David" +title After command "add n/David p/99998888 e/david@gmail.com" package States <> { class State1 as "ab0:AddressBook" diff --git a/docs/diagrams/UndoRedoState4.puml b/docs/diagrams/UndoRedoState4.puml index 2bc631ffcd0..301da5a2b40 100644 --- a/docs/diagrams/UndoRedoState4.puml +++ b/docs/diagrams/UndoRedoState4.puml @@ -4,7 +4,7 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 skinparam ClassBackgroundColor #FFFFAA -title After command "list" +title After command "liststudents" package States <> { class State1 as "ab0:AddressBook" diff --git a/docs/diagrams/tracing/LogicSequenceDiagram.puml b/docs/diagrams/tracing/LogicSequenceDiagram.puml index 42bf46d3ce8..acbf5532bc4 100644 --- a/docs/diagrams/tracing/LogicSequenceDiagram.puml +++ b/docs/diagrams/tracing/LogicSequenceDiagram.puml @@ -14,7 +14,7 @@ create ecp abp -> ecp abp -> ecp ++: parse(arguments) create ec -ecp -> ec ++: index, editPersonDescriptor +ecp -> ec ++: index, editStudentDescriptor ec --> ecp -- ecp --> abp --: command abp --> logic --: command diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 37ad06a2803..dad3eb56775 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 index 02a42e35e76..5a06c10a1ae 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/Consultation/ConsultCommands.png b/docs/images/Consultation/ConsultCommands.png new file mode 100644 index 00000000000..33c2b8aec6e Binary files /dev/null and b/docs/images/Consultation/ConsultCommands.png differ diff --git a/docs/images/Consultation/SequenceDiagramAddToConsultCommand.png b/docs/images/Consultation/SequenceDiagramAddToConsultCommand.png new file mode 100644 index 00000000000..c89e4979205 Binary files /dev/null and b/docs/images/Consultation/SequenceDiagramAddToConsultCommand.png differ diff --git a/docs/images/Consultation/addStudent_for_consult.png b/docs/images/Consultation/addStudent_for_consult.png new file mode 100644 index 00000000000..c598980ca38 Binary files /dev/null and b/docs/images/Consultation/addStudent_for_consult.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index ac2ae217c51..3096a253120 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/FullerLogicClassDiagram.png b/docs/images/FullerLogicClassDiagram.png new file mode 100644 index 00000000000..f29a729d1de Binary files /dev/null and b/docs/images/FullerLogicClassDiagram.png differ diff --git a/docs/images/Lessons/LessonsAndRelatedCommands.png b/docs/images/Lessons/LessonsAndRelatedCommands.png new file mode 100644 index 00000000000..c17d9fe80f1 Binary files /dev/null and b/docs/images/Lessons/LessonsAndRelatedCommands.png differ diff --git a/docs/images/Lessons/MarkAttendanceCommandSequenceDiagram.png b/docs/images/Lessons/MarkAttendanceCommandSequenceDiagram.png new file mode 100644 index 00000000000..e39678118ed Binary files /dev/null and b/docs/images/Lessons/MarkAttendanceCommandSequenceDiagram.png differ diff --git a/docs/images/Lessons/SequenceDiagramAddToLessonCommand.png b/docs/images/Lessons/SequenceDiagramAddToLessonCommand.png new file mode 100644 index 00000000000..6e89eb402cb Binary files /dev/null and b/docs/images/Lessons/SequenceDiagramAddToLessonCommand.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index fe91c69efe7..3588a37b65d 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..e322002089a 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..54aba7fa520 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/UgConsultation.png b/docs/images/UgConsultation.png new file mode 100644 index 00000000000..9eb8e147f8f Binary files /dev/null and b/docs/images/UgConsultation.png differ diff --git a/docs/images/UgLesson.png b/docs/images/UgLesson.png new file mode 100644 index 00000000000..966b2706808 Binary files /dev/null and b/docs/images/UgLesson.png differ diff --git a/docs/images/UgQuickStartDownload.png b/docs/images/UgQuickStartDownload.png new file mode 100644 index 00000000000..44a211b6637 Binary files /dev/null and b/docs/images/UgQuickStartDownload.png differ diff --git a/docs/images/UgRedConsultation.png b/docs/images/UgRedConsultation.png new file mode 100644 index 00000000000..fa828b261ad Binary files /dev/null and b/docs/images/UgRedConsultation.png differ diff --git a/docs/images/UgRedLesson.png b/docs/images/UgRedLesson.png new file mode 100644 index 00000000000..0ca3a6cd171 Binary files /dev/null and b/docs/images/UgRedLesson.png differ diff --git a/docs/images/UgStudent.png b/docs/images/UgStudent.png new file mode 100644 index 00000000000..31a0b741f13 Binary files /dev/null and b/docs/images/UgStudent.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..9943c39f824 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..e0b29feb5a3 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png index 20853694e03..8ed1f1511e3 100644 Binary files a/docs/images/UndoRedoState2.png and b/docs/images/UndoRedoState2.png differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png index 46dfae78c94..40fe0ca5208 100644 Binary files a/docs/images/UndoRedoState4.png and b/docs/images/UndoRedoState4.png differ diff --git a/docs/images/addExample.png b/docs/images/addExample.png new file mode 100644 index 00000000000..812c31a00c1 Binary files /dev/null and b/docs/images/addExample.png differ diff --git a/docs/images/addconsultExample.png b/docs/images/addconsultExample.png new file mode 100644 index 00000000000..c85b4204672 Binary files /dev/null and b/docs/images/addconsultExample.png differ diff --git a/docs/images/addconsultExamplePast.png b/docs/images/addconsultExamplePast.png new file mode 100644 index 00000000000..63122f93b04 Binary files /dev/null and b/docs/images/addconsultExamplePast.png differ diff --git a/docs/images/addlessonExample.png b/docs/images/addlessonExample.png new file mode 100644 index 00000000000..05fce8e5144 Binary files /dev/null and b/docs/images/addlessonExample.png differ diff --git a/docs/images/addlessonExamplePast.png b/docs/images/addlessonExamplePast.png new file mode 100644 index 00000000000..81ac44656a0 Binary files /dev/null and b/docs/images/addlessonExamplePast.png differ diff --git a/docs/images/addtoconsultExample.png b/docs/images/addtoconsultExample.png new file mode 100644 index 00000000000..6f45fab5f9b Binary files /dev/null and b/docs/images/addtoconsultExample.png differ diff --git a/docs/images/addtolessonExample.png b/docs/images/addtolessonExample.png new file mode 100644 index 00000000000..9aef1c8cc38 Binary files /dev/null and b/docs/images/addtolessonExample.png differ diff --git a/docs/images/clarenceeey.png b/docs/images/clarenceeey.png new file mode 100644 index 00000000000..7c2f301b5be Binary files /dev/null and b/docs/images/clarenceeey.png differ diff --git a/docs/images/deleteExample.png b/docs/images/deleteExample.png new file mode 100644 index 00000000000..b58da404755 Binary files /dev/null and b/docs/images/deleteExample.png differ diff --git a/docs/images/deleteconsultExample.png b/docs/images/deleteconsultExample.png new file mode 100644 index 00000000000..3c25e927c60 Binary files /dev/null and b/docs/images/deleteconsultExample.png differ diff --git a/docs/images/deletelessonExample.png b/docs/images/deletelessonExample.png new file mode 100644 index 00000000000..311607ff238 Binary files /dev/null and b/docs/images/deletelessonExample.png differ diff --git a/docs/images/editExample.png b/docs/images/editExample.png new file mode 100644 index 00000000000..19ed0f23e91 Binary files /dev/null and b/docs/images/editExample.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/findCourseExample.png b/docs/images/findCourseExample.png new file mode 100644 index 00000000000..c8cfd8cb7b5 Binary files /dev/null and b/docs/images/findCourseExample.png differ diff --git a/docs/images/findNameExample.png b/docs/images/findNameExample.png new file mode 100644 index 00000000000..fde21ab5733 Binary files /dev/null and b/docs/images/findNameExample.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..e605da93992 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/liststudentsExample.png b/docs/images/liststudentsExample.png new file mode 100644 index 00000000000..165ffbfa939 Binary files /dev/null and b/docs/images/liststudentsExample.png differ diff --git a/docs/images/marcusjhang.png b/docs/images/marcusjhang.png new file mode 100644 index 00000000000..a79271877f0 Binary files /dev/null and b/docs/images/marcusjhang.png differ diff --git a/docs/images/markaExample.png b/docs/images/markaExample.png new file mode 100644 index 00000000000..4f089628c59 Binary files /dev/null and b/docs/images/markaExample.png differ diff --git a/docs/images/markpExample.png b/docs/images/markpExample.png new file mode 100644 index 00000000000..8f75aadc9f9 Binary files /dev/null and b/docs/images/markpExample.png differ diff --git a/docs/images/notnotmax.png b/docs/images/notnotmax.png new file mode 100644 index 00000000000..cb9fe8be032 Binary files /dev/null and b/docs/images/notnotmax.png differ diff --git a/docs/images/removefromconsultExample.png b/docs/images/removefromconsultExample.png new file mode 100644 index 00000000000..131048a0f59 Binary files /dev/null and b/docs/images/removefromconsultExample.png differ diff --git a/docs/images/removefromlessonExample.png b/docs/images/removefromlessonExample.png new file mode 100644 index 00000000000..07c52894b8b Binary files /dev/null and b/docs/images/removefromlessonExample.png differ diff --git a/docs/images/s-k-y-light.png b/docs/images/s-k-y-light.png new file mode 100644 index 00000000000..09346d2c107 Binary files /dev/null and b/docs/images/s-k-y-light.png differ diff --git a/docs/images/yhanyi.png b/docs/images/yhanyi.png new file mode 100644 index 00000000000..3df66e6fe64 Binary files /dev/null and b/docs/images/yhanyi.png differ diff --git a/docs/img.png b/docs/img.png new file mode 100644 index 00000000000..1df80cf4abf Binary files /dev/null and b/docs/img.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..bd490727b8b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- layout: page -title: AddressBook Level-3 +title: TA-Hub --- [![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) diff --git a/docs/team/clarence.md b/docs/team/clarence.md new file mode 100644 index 00000000000..773a07794e2 --- /dev/null +++ b/docs/team/clarence.md @@ -0,0 +1,46 @@ +--- +layout: page +title: John Doe's Project Portfolio Page +--- + +### Project: AddressBook Level 3 + +AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + +* **Code contributed**: [RepoSense link]() + +* **Project management**: + * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) + * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) + +* **Documentation**: + * User Guide: + * Added documentation for the features `delete` and `find` [\#72]() + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `delete` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() + * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) + * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) + * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) + +* **Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo + +* _{you can add/remove categories in the list above}_ diff --git a/docs/team/marcusjhang.md b/docs/team/marcusjhang.md new file mode 100644 index 00000000000..0e99a3b23a1 --- /dev/null +++ b/docs/team/marcusjhang.md @@ -0,0 +1,46 @@ +--- +layout: page +title: Marcus Ang's Project Portfolio Page +--- + +### Project: AddressBook Level 3 + +AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + +* **Code contributed**: [RepoSense link]() + +* **Project management**: + * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) + * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) + +* **Documentation**: + * User Guide: + * Added documentation for the features `delete` and `find` [\#72]() + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `delete` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() + * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) + * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) + * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) + +* **Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo + +* _{you can add/remove categories in the list above}_ diff --git a/docs/team/notnotmax.md b/docs/team/notnotmax.md new file mode 100644 index 00000000000..f1c32af1d29 --- /dev/null +++ b/docs/team/notnotmax.md @@ -0,0 +1,11 @@ +--- +layout: page +title: notnotmax's Project Portfolio Page +--- + +### Project: TAHub + +TAHub is a desktop application. +The user interacts with it using a CLI, and it has a GUI created with JavaFX. + +Given below are my contributions to the project. diff --git a/docs/team/sky.md b/docs/team/sky.md new file mode 100644 index 00000000000..d4a3fdd2198 --- /dev/null +++ b/docs/team/sky.md @@ -0,0 +1,46 @@ +--- +layout: page +title: Sky Lim's Project Portfolio Page +--- + +### Project: TAHub + +TAHub - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + +* **Code contributed**: [RepoSense link]() + +* **Project management**: + * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) + * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) + +* **Documentation**: + * User Guide: + * Added documentation for the features `delete` and `find` [\#72]() + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `delete` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() + * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) + * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) + * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) + +* **Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo + +* _{you can add/remove categories in the list above}_ diff --git a/docs/team/yhanyi.md b/docs/team/yhanyi.md new file mode 100644 index 00000000000..dbc73533f07 --- /dev/null +++ b/docs/team/yhanyi.md @@ -0,0 +1,46 @@ +--- +layout: page +title: Yeoh Han Yi's Project Portfolio Page +--- + +### Project: TAHub + +TAHub is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + +* **Code contributed**: [RepoSense link]() + +* **Project management**: + * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) + * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) + +* **Documentation**: + * User Guide: + * Added documentation for the features `delete` and `find` [\#72]() + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `delete` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() + * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) + * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) + * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) + +* **Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo + +* _{you can add/remove categories in the list above}_ diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/seedu/address/commons/core/index/Index.java index dd170d8b68d..1009d5d68a7 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/seedu/address/commons/core/index/Index.java @@ -66,4 +66,9 @@ public boolean equals(Object other) { public String toString() { return new ToStringBuilder(this).add("zeroBasedIndex", zeroBasedIndex).toString(); } + + @Override + public int hashCode() { + return this.zeroBasedIndex; + } } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..e0404456dde 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -8,7 +8,9 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; +import seedu.address.model.consultation.Consultation; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.student.Student; /** * API of the Logic component @@ -30,8 +32,14 @@ public interface Logic { */ ReadOnlyAddressBook getAddressBook(); - /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of students */ + ObservableList getFilteredStudentList(); + + /** Returns an unmodifiable view of the filtered list of students */ + ObservableList getFilteredConsultationList(); + + /** Returns an unmodifiable view of the filtered list of lessons */ + ObservableList getFilteredLessonList(); /** * Returns the user prefs' address book file path. diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..25bb8ad9fe6 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -15,7 +15,9 @@ 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.model.consultation.Consultation; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.student.Student; import seedu.address.storage.Storage; /** @@ -67,10 +69,19 @@ public ReadOnlyAddressBook getAddressBook() { } @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); + public ObservableList getFilteredStudentList() { + return model.getFilteredStudentList(); } + @Override + public ObservableList getFilteredConsultationList() { + return model.getFilteredConsultationList(); + } + + @Override + public ObservableList getFilteredLessonList() { + return model.getFilteredLessonList(); + } @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..2303df495b7 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -5,7 +5,9 @@ import java.util.stream.Stream; import seedu.address.logic.parser.Prefix; -import seedu.address.model.person.Person; +import seedu.address.model.consultation.Consultation; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.student.Student; /** * Container for user visible messages. @@ -14,10 +16,16 @@ 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): "; + public static final String MESSAGE_INVALID_STUDENT_DISPLAYED_INDEX = "The student provided " + + "at index %1$d is invalid"; + public static final String MESSAGE_INVALID_INDEX_SHOWN = "The target provided at index/indices %1$s is invalid"; + public static final String MESSAGE_STUDENTS_LISTED_OVERVIEW = "%1$d students listed!"; + public static final String MESSAGE_DUPLICATE_FIELDS = "Multiple values specified for " + + "the following single-valued field(s): "; + public static final String MESSAGE_INVALID_CONSULTATION_DISPLAYED_INDEX = "The consultation provided " + + "at index %1$d is invalid"; + public static final String MESSAGE_INVALID_LESSON_DISPLAYED_INDEX = "The lesson provided at index %1$d is invalid"; + public static final String MESSAGE_LESSONS_LISTED_OVERVIEW = "%1$d lessons listed!"; /** * Returns an error message indicating the duplicate prefixes. @@ -25,27 +33,60 @@ public class Messages { public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePrefixes) { assert duplicatePrefixes.length > 0; - Set duplicateFields = - Stream.of(duplicatePrefixes).map(Prefix::toString).collect(Collectors.toSet()); + 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. + * Formats the {@code student} for display to the user. */ - public static String format(Person person) { + public static String format(Student student) { final StringBuilder builder = new StringBuilder(); - builder.append(person.getName()) + builder.append(student.getName()) .append("; Phone: ") - .append(person.getPhone()) + .append(student.getPhone()) .append("; Email: ") - .append(person.getEmail()) - .append("; Address: ") - .append(person.getAddress()) - .append("; Tags: "); - person.getTags().forEach(builder::append); + .append(student.getEmail()); + if (!student.getCourses().isEmpty()) { + builder.append("; Courses: "); + student.getCourses().forEach(course -> builder.append(course).append(", ")); + builder.setLength(builder.length() - 2); // Remove trailing comma and space + } + return builder.toString().trim(); + } + + /** + * Formats the {@code consult} for display to the user. + */ + public static String format(Consultation consult) { + final StringBuilder builder = new StringBuilder(); + builder.append("Date: ") + .append(consult.getDate().toString()) + .append("; Time: ") + .append(consult.getTime().toString()); + if (!consult.getStudents().isEmpty()) { + builder.append("; Students: "); + consult.getStudents().forEach(student -> builder.append(student.getName()).append(", ")); + builder.setLength(builder.length() - 2); // Remove trailing comma and space + } return builder.toString(); } + /** + * Formats the {@code lesson} for display to the user. + */ + public static String format(Lesson lesson) { + final StringBuilder builder = new StringBuilder(); + builder.append("Date: ") + .append(lesson.getDate().toString()) + .append("; Time: ") + .append(lesson.getTime().toString()); + if (!lesson.getStudents().isEmpty()) { + builder.append("; Students: "); + lesson.getStudents().forEach(student -> builder.append(student.getName()).append(", ")); + builder.setLength(builder.length() - 2); // Remove trailing comma and space + } + return builder.toString(); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..62b379e2976 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -1,63 +1,70 @@ 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_COURSE; 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; +import seedu.address.model.student.Student; /** - * Adds a person to the address book. + * Adds a student to the address book. */ public class AddCommand extends Command { public static final String COMMAND_WORD = "add"; + public static final CommandType COMMAND_TYPE = CommandType.STUDENT; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a student to the address book. " + "Parameters: " + PREFIX_NAME + "NAME " + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_COURSE + "COURSE]...\n" + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_EMAIL + "johnd@example.com "; - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + public static final String MESSAGE_SUCCESS = "New student added: %1$s"; + public static final String MESSAGE_DUPLICATE_STUDENT = "This student already exists in the address book"; - private final Person toAdd; + private final Student toAdd; /** - * Creates an AddCommand to add the specified {@code Person} + * Creates an AddCommand to add the specified {@code Student} */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; + public AddCommand(Student student) { + requireNonNull(student); + toAdd = student; + } + + /** + * Returns Command Type ADDSTUDENT + * + * @return Command Type ADDSTUDENT + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; } @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + if (model.hasStudent(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_STUDENT); } - model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); + model.addStudent(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd)), + COMMAND_TYPE); } @Override diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..b6dfdbabf25 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -11,13 +11,23 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; + public static final CommandType COMMAND_TYPE = CommandType.CLEAR; public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + /** + * Returns Command Type CLEAR + * + * @return Command Type CLEAR + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } @Override public CommandResult execute(Model model) { requireNonNull(model); model.setAddressBook(new AddressBook()); - return new CommandResult(MESSAGE_SUCCESS); + return new CommandResult(MESSAGE_SUCCESS, COMMAND_TYPE); } } diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java index 64f18992160..bb25259c047 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/address/logic/commands/Command.java @@ -17,4 +17,10 @@ public abstract class Command { */ public abstract CommandResult execute(Model model) throws CommandException; + /** + * Returns Type of Command + * + * @return Type of Command + */ + public abstract CommandType getCommandType(); } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 249b6072d0d..83896de1338 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -4,6 +4,7 @@ import java.util.Objects; +import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; /** @@ -13,39 +14,49 @@ public class CommandResult { private final String feedbackToUser; - /** Help information should be shown to the user. */ - private final boolean showHelp; + /** Command Type, used for deciding TAHub UI Action */ + private final CommandType commandType; - /** The application should exit. */ - private final boolean exit; + /** Index of Tab */ + private final Index tabIndex; /** - * Constructs a {@code CommandResult} with the specified fields. + * Constructs a {@code CommandResult} with the specified {@code feedbacktoUser}, + * {@code commandType} and {@code tabIndex} + * + * @param feedbackToUser FeedbacktoUser + * @param commandType Command Type + * @param tabIndex Tab Index */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, CommandType commandType, Index tabIndex) { this.feedbackToUser = requireNonNull(feedbackToUser); - this.showHelp = showHelp; - this.exit = exit; + this.commandType = commandType; + this.tabIndex = tabIndex; } /** - * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, - * and other fields set to their default value. + * Constructs a {@code CommandResult} with the specified {@code feedbacktoUser}, + * {@code commandType} and other fields set to their default value. + * + * @param feedbackToUser FeedbacktoUser + * @param commandType Command Type */ - public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + public CommandResult(String feedbackToUser, CommandType commandType) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.commandType = commandType; + this.tabIndex = null; } public String getFeedbackToUser() { return feedbackToUser; } - public boolean isShowHelp() { - return showHelp; + public CommandType getCommandType() { + return commandType; } - public boolean isExit() { - return exit; + public Index getTabIndex() { + return tabIndex; } @Override @@ -61,21 +72,20 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) - && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && commandType == otherCommandResult.commandType; } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, commandType); } @Override public String toString() { return new ToStringBuilder(this) .add("feedbackToUser", feedbackToUser) - .add("showHelp", showHelp) - .add("exit", exit) + .add("commandType", commandType) + .add("tabIndex", tabIndex) .toString(); } diff --git a/src/main/java/seedu/address/logic/commands/CommandType.java b/src/main/java/seedu/address/logic/commands/CommandType.java new file mode 100644 index 00000000000..7a33d8de6d8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CommandType.java @@ -0,0 +1,17 @@ +package seedu.address.logic.commands; + +/** + * Enum representing the types of commmand + */ +public enum CommandType { + // Command Type for General Use + CLEAR, + HELP, + EXIT, + // Command Type for Students + STUDENT, + // Command Type for Consultations + CONSULT, + // Command Type for Lesson + LESSON +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..0f2c29d24a4 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -1,48 +1,87 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.DEFAULT_DELIMITER; +import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; 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; +import seedu.address.model.student.Student; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes a student 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 CommandType COMMAND_TYPE = CommandType.STUDENT; 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"; + + ": Deletes the student identified by the index number used in the displayed student list.\n" + + "Parameters: INDEX (must be a positive integer) [;INDEX...]\n" + + "Example: " + COMMAND_WORD + " 1" + DEFAULT_DELIMITER + "2"; - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_DELETE_STUDENT_SUCCESS = "Deleted Student(s):\n%1$s"; - private final Index targetIndex; + private final Set targetIndices; - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; + public DeleteCommand(Set targetIndices) { + this.targetIndices = targetIndices; + } + + /** + * Returns Command Type DELETESTUDENT + * + * @return Command Type DELETESTUDENT + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; } @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); + List lastShownList = model.getFilteredStudentList(); + + boolean throwException = false; + ArrayList outOfBounds = new ArrayList<>(); + + for (Index item: targetIndices) { + if (item.getZeroBased() >= lastShownList.size()) { + throwException = true; + outOfBounds.add(item); + } + } - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + if (throwException) { + String formattedOutOfBoundIndices = outOfBounds.stream() + .map(index -> String.valueOf(index.getOneBased())) + .collect(Collectors.joining(", ")); + throw new CommandException(String.format(Messages.MESSAGE_INVALID_INDEX_SHOWN, formattedOutOfBoundIndices)); } - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); + List deletedPeople = targetIndices.stream() + .map(targetIndex -> lastShownList.get(targetIndex.getZeroBased())) + .toList(); + + + deletedPeople.forEach(model::deleteStudent); + + String formattedDeletedPeople = deletedPeople.stream() + .map(Messages::format) + .collect(Collectors.joining("\n")); + + + return new CommandResult(String.format(MESSAGE_DELETE_STUDENT_SUCCESS, formattedDeletedPeople), + COMMAND_TYPE); } @Override @@ -57,13 +96,13 @@ public boolean equals(Object other) { } DeleteCommand otherDeleteCommand = (DeleteCommand) other; - return targetIndex.equals(otherDeleteCommand.targetIndex); + return targetIndices.equals(otherDeleteCommand.targetIndices); } @Override public String toString() { return new ToStringBuilder(this) - .add("targetIndex", targetIndex) + .add("targetIndices", targetIndices) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..1ebed7a15c1 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -1,12 +1,10 @@ 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_COURSE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import java.util.Collections; import java.util.HashSet; @@ -21,87 +19,98 @@ 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 seedu.address.model.course.Course; +import seedu.address.model.student.Email; +import seedu.address.model.student.Name; +import seedu.address.model.student.Phone; +import seedu.address.model.student.Student; /** - * Edits the details of an existing person in the address book. + * Edits the details of an existing student in the address book. */ public class EditCommand extends Command { public static final String COMMAND_WORD = "edit"; + public static final CommandType COMMAND_TYPE = CommandType.STUDENT; - 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. " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the student identified " + + "by the index number used in the displayed student list. " + "Existing values will be overwritten by the input values.\n" + "Parameters: INDEX (must be a positive integer) " + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_COURSE + "COURSE]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; + + PREFIX_EMAIL + "johndoe@example.com " + + PREFIX_COURSE + "CS2103T;CS2101"; - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; + public static final String MESSAGE_EDIT_STUDENT_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."; + public static final String MESSAGE_DUPLICATE_STUDENT = "This student already exists in the address book."; private final Index index; - private final EditPersonDescriptor editPersonDescriptor; + private final EditStudentDescriptor editStudentDescriptor; /** - * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with + * @param index of the student in the filtered student list to edit + * @param editStudentDescriptor details to edit the student with */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { + public EditCommand(Index index, EditStudentDescriptor editStudentDescriptor) { requireNonNull(index); - requireNonNull(editPersonDescriptor); + requireNonNull(editStudentDescriptor); this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); + this.editStudentDescriptor = new EditStudentDescriptor(editStudentDescriptor); + } + + /** + * Returns Command Type EDITSTUDENT + * + * @return Command Type EDITSTUDENT + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; } @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); + List lastShownList = model.getFilteredStudentList(); if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + throw new CommandException(String.format(Messages.MESSAGE_INVALID_STUDENT_DISPLAYED_INDEX, + index.getOneBased())); } - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + Student studentToEdit = lastShownList.get(index.getZeroBased()); + Student editedStudent = createEditedStudent(studentToEdit, editStudentDescriptor); - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + if (!studentToEdit.isSameStudent(editedStudent) && model.hasStudent(editedStudent)) { + throw new CommandException(MESSAGE_DUPLICATE_STUDENT); } - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); + model.setStudent(studentToEdit, editedStudent); + return new CommandResult(String.format(MESSAGE_EDIT_STUDENT_SUCCESS, Messages.format(editedStudent)), + COMMAND_TYPE); } /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. + * Creates and returns a {@code Student} with the details of {@code studentToEdit} + * edited with {@code editStudentDescriptor}. */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + private static Student createEditedStudent(Student studentToEdit, EditStudentDescriptor editStudentDescriptor) { + assert studentToEdit != null; + + Name updatedName = editStudentDescriptor.getName().orElse(studentToEdit.getName()); + Phone updatedPhone = editStudentDescriptor.getPhone().orElse(studentToEdit.getPhone()); + Email updatedEmail = editStudentDescriptor.getEmail().orElse(studentToEdit.getEmail()); + Set updatedCourses = editStudentDescriptor.getCourses().orElse(studentToEdit.getCourses()); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Student(updatedName, updatedPhone, updatedEmail, updatedCourses); } @Override @@ -117,47 +126,45 @@ public boolean equals(Object other) { EditCommand otherEditCommand = (EditCommand) other; return index.equals(otherEditCommand.index) - && editPersonDescriptor.equals(otherEditCommand.editPersonDescriptor); + && editStudentDescriptor.equals(otherEditCommand.editStudentDescriptor); } @Override public String toString() { return new ToStringBuilder(this) .add("index", index) - .add("editPersonDescriptor", editPersonDescriptor) + .add("editStudentDescriptor", editStudentDescriptor) .toString(); } /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. + * Stores the details to edit the student with. Each non-empty field value will replace the + * corresponding field value of the student. */ - public static class EditPersonDescriptor { + public static class EditStudentDescriptor { private Name name; private Phone phone; private Email email; - private Address address; - private Set tags; + private Set courses; - public EditPersonDescriptor() {} + public EditStudentDescriptor() {} /** * Copy constructor. * A defensive copy of {@code tags} is used internally. */ - public EditPersonDescriptor(EditPersonDescriptor toCopy) { + public EditStudentDescriptor(EditStudentDescriptor toCopy) { setName(toCopy.name); setPhone(toCopy.phone); setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); + setCourses(toCopy.courses); } /** * 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, courses); } public void setName(Name name) { @@ -184,29 +191,21 @@ public Optional getEmail() { return Optional.ofNullable(email); } - public void setAddress(Address address) { - this.address = address; - } - - public Optional
getAddress() { - return Optional.ofNullable(address); - } - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. + * Sets {@code courses} to this object's {@code courses}. + * A defensive copy of {@code courses} is used internally. */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; + public void setCourses(Set courses) { + this.courses = (courses != null) ? new HashSet<>(courses) : null; } /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * Returns an unmodifiable course set, which throws {@code UnsupportedOperationException} * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. + * Returns {@code Optional#empty()} if {@code courses} is null. */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + public Optional> getCourses() { + return (courses != null) ? Optional.of(Collections.unmodifiableSet(courses)) : Optional.empty(); } @Override @@ -216,16 +215,15 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { + if (!(other instanceof EditStudentDescriptor)) { 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); + EditStudentDescriptor otherEditStudentDescriptor = (EditStudentDescriptor) other; + return Objects.equals(name, otherEditStudentDescriptor.name) + && Objects.equals(phone, otherEditStudentDescriptor.phone) + && Objects.equals(email, otherEditStudentDescriptor.email) + && Objects.equals(courses, otherEditStudentDescriptor.courses); } @Override @@ -234,8 +232,7 @@ public String toString() { .add("name", name) .add("phone", phone) .add("email", email) - .add("address", address) - .add("tags", tags) + .add("courses", courses) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..43b81e1b309 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -8,12 +8,22 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; - + public static final CommandType COMMAND_TYPE = CommandType.EXIT; public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; + /** + * Returns Command Type EXIT + * + * @return Command Type EXIT + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, COMMAND_TYPE); } } diff --git a/src/main/java/seedu/address/logic/commands/ExportCommand.java b/src/main/java/seedu/address/logic/commands/ExportCommand.java new file mode 100644 index 00000000000..22ea0a53b77 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ExportCommand.java @@ -0,0 +1,224 @@ +package seedu.address.logic.commands; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.course.Course; +import seedu.address.model.student.Student; + +/** + * Exports the currently displayed list of students to a CSV file. + */ +public class ExportCommand extends Command { + + public static final String COMMAND_WORD = "export"; + public static final String FORCE_FLAG = "-f"; + public static final CommandType COMMAND_TYPE = CommandType.STUDENT; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Exports the current list of students to a CSV file. " + + "Parameters: FILENAME " + "[" + FORCE_FLAG + "] " + + "\nExample: " + COMMAND_WORD + " students" + + "\nExample with force flag: " + COMMAND_WORD + " " + FORCE_FLAG + " students"; + + public static final String MESSAGE_SUCCESS = "Exported %1$d students to %2$s"; + public static final String MESSAGE_FAILURE = "Failed to export students: %1$s"; + public static final String MESSAGE_FILE_EXISTS = "File %1$s already exists. Use -f flag to overwrite."; + public static final String MESSAGE_HOME_FILE_EXISTS = + "File %1$s already exists in home directory. Use -f flag to overwrite."; + public static final String MESSAGE_SUCCESS_WITH_COPY = "Exported %1$d students to %2$s and %3$s"; + public static final String INVALID_FILENAME_MESSAGE = + "Filename can only contain alphanumeric characters (A-Z, a-z, 0-9)"; + private static final Logger logger = LogsCenter.getLogger(ExportCommand.class); + + private final String filename; + private final boolean isForceExport; + private final Path baseDir; + + /** + * Creates an ExportCommand to export data to the specified filename in the default directory + */ + public ExportCommand(String filename, boolean isForceExport) { + this(filename, isForceExport, Paths.get("data")); + } + + /** + * Creates an ExportCommand to export data to the specified filename in the specified directory + * This constructor is primarily for testing purposes + */ + public ExportCommand(String filename, boolean isForceExport, Path baseDir) { + this.filename = filename; + this.isForceExport = isForceExport; + this.baseDir = baseDir; + } + + /** + * Returns Command Type EXPORTSTUDENT + * + * @return Command Type EXPORTSTUDENT + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + /** + * Gets the home file path. Protected for testing purposes. + * @param filename The name of the file (without extension) + * @return The full path to the file in the home directory + */ + protected Path getHomeFilePath(String filename) { + return Paths.get(System.getProperty("user.home"), filename + ".csv"); + } + + /** + * Validates if filename is valid + * + * @param filename String representing filename to be validated + */ + protected void validateFilename(String filename) throws CommandException { + if (!filename.matches("^[a-zA-Z0-9]+$")) { + throw new CommandException(INVALID_FILENAME_MESSAGE); + } + } + + @Override + public CommandResult execute(Model model) throws CommandException { + assert model != null : "Model cannot be null"; + validateFilename(filename); + List studentList = model.getFilteredStudentList(); + logger.info("Starting export for " + studentList.size() + " students"); + + Path dataFilePath = baseDir.resolve(filename + ".csv"); + Path homeFilePath = getHomeFilePath(filename); + logger.fine("Export paths - Data: " + dataFilePath + ", Home: " + homeFilePath); + + // Create directories if they don't exist + try { + Files.createDirectories(baseDir); + logger.fine("Created directory: " + baseDir); + } catch (IOException e) { + logger.warning("Failed to create directory: " + baseDir); + throw new CommandException(String.format(MESSAGE_FAILURE, "Could not create directory: " + e.getMessage())); + } + + // Check both locations for existing files when force flag is not set + if (!isForceExport) { + if (Files.exists(dataFilePath)) { + logger.info("Data file already exists: " + dataFilePath); + throw new CommandException(String.format(MESSAGE_FILE_EXISTS, dataFilePath)); + } + if (Files.exists(homeFilePath)) { + logger.info("Home file already exists: " + homeFilePath); + throw new CommandException(String.format(MESSAGE_HOME_FILE_EXISTS, homeFilePath)); + } + } + + try { + // Write to data directory + writeCsvFile(dataFilePath, studentList); + logger.info("Successfully wrote data to: " + dataFilePath); + + // Copy to home directory + try { + if (isForceExport) { + Files.copy(dataFilePath, homeFilePath, StandardCopyOption.REPLACE_EXISTING); + logger.info("Force-copied file to home directory: " + homeFilePath); + } else { + Files.copy(dataFilePath, homeFilePath); + logger.info("Copied file to home directory: " + homeFilePath); + } + return new CommandResult(String.format(MESSAGE_SUCCESS_WITH_COPY, + studentList.size(), dataFilePath, homeFilePath), + COMMAND_TYPE); + } catch (IOException e) { + // If home directory copy fails for other reasons, return success with data file only + return new CommandResult(String.format(MESSAGE_SUCCESS, studentList.size(), dataFilePath), + COMMAND_TYPE); + } + } catch (IOException e) { + logger.warning("Failed to write CSV file: " + e.getMessage()); + // Clean up any partially created files + try { + Files.deleteIfExists(dataFilePath); + } catch (IOException ignored) { + // Ignore cleanup errors + } + throw new CommandException(String.format(MESSAGE_FAILURE, e.getMessage())); + } + } + + String escapeSpecialCharacters(String data) { + assert data != null : "Input string cannot be null"; + String escapedData = data.replaceAll("\\R", " "); + if (data.contains(",") || data.contains("\"") || data.contains("'")) { + data = data.replace("\"", "\"\""); + escapedData = "\"" + data + "\""; + } + assert escapedData != null : "Escaped data cannot be null"; + return escapedData; + } + + /** + * Writes all Students in a given List into a CSV file saved at filePath + * @param filePath Location to save CSV file + * @param studentList List of Students whose details should be saved + * @throws IOException Throws IO Exception + */ + public void writeCsvFile(Path filePath, List studentList) throws IOException { + assert filePath != null : "File path cannot be null"; + assert studentList != null : "Student list cannot be null"; + logger.fine("Writing CSV file to: " + filePath); + try (FileWriter csvWriter = new FileWriter(filePath.toFile())) { + // Write CSV header + csvWriter.append("Name,Phone,Email,Courses\n"); + logger.finest("Wrote CSV header"); + + // Write student data + for (Student student : studentList) { + assert student != null : "Student cannot be null"; + csvWriter.append(String.format("%s,%s,%s,%s\n", + escapeSpecialCharacters(student.getName().fullName), + student.getPhone().value, + student.getEmail().value, + escapeSpecialCharacters(coursesToString(student.getCourses())))); + } + csvWriter.flush(); + logger.finest("Wrote " + studentList.size() + " student records"); + } + } + + /** + * Helper function that collects a set of Courses into a string for exporting + * @param courses Set of Courses to be collected into a string + * @return String representing a set of Courses + */ + public String coursesToString(Set courses) { + assert courses != null : "Courses set cannot be null"; + return courses.stream() + .map(Course::toString) + .collect(Collectors.joining(";")); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof ExportCommand)) { + return false; + } + ExportCommand otherCommand = (ExportCommand) other; + return filename.equals(otherCommand.filename) + && isForceExport == otherCommand.isForceExport; + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..d5ed82c7216 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -1,37 +1,90 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.DEFAULT_DELIMITER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COURSE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.List; +import java.util.function.Predicate; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.student.Student; /** - * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. + * Finds and lists all students 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 CommandType COMMAND_TYPE = CommandType.STUDENT; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all students 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"; + + "Parameters: KEYWORD PARAMETERS [;PARAMETERS...] [MORE_KEYWORDS_WITH_PARAMETERS]...\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "alice" + DEFAULT_DELIMITER + "bob" + + PREFIX_COURSE + "CS2100" + DEFAULT_DELIMITER + "CS2040"; + + /** Raw unmodified list of predicates for use in equality checking */ + private final List> predicates; + /** Protected for usage in testcases */ + private final Predicate combinedPredicate; + + /** + * Creates a FindCommand given a single predicate. + * + * @param predicate Predicate. + */ + public FindCommand(Predicate predicate) { + this.predicates = List.of(predicate); + this.combinedPredicate = predicate; + } + + /** + * Creates a FindCommand given a list of predicates. + * + * @param predicates List of predicates. + */ + public FindCommand(List> predicates) { + this.predicates = predicates; + this.combinedPredicate = combinePredicates(predicates); + } - private final NameContainsKeywordsPredicate predicate; + public Predicate getPredicate() { + return student -> combinedPredicate.test(student); + } - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; + /** + * Returns a predicate made by chaining together the given predicates. + * + * @param predicates List of predicates. + * @return Combined predicate. + */ + private Predicate combinePredicates(List> predicates) { + return student -> predicates.stream().allMatch(p -> p.test(student)); + } + + /** + * Returns Command Type FINDSTUDENT + * + * @return Command Type FINDSTUDENT + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; } @Override public CommandResult execute(Model model) { requireNonNull(model); - model.updateFilteredPersonList(predicate); + model.updateFilteredStudentList(combinedPredicate); return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + String.format(Messages.MESSAGE_STUDENTS_LISTED_OVERVIEW, model.getFilteredStudentList().size()), + COMMAND_TYPE); } @Override @@ -46,13 +99,13 @@ public boolean equals(Object other) { } FindCommand otherFindCommand = (FindCommand) other; - return predicate.equals(otherFindCommand.predicate); + return predicates.equals(otherFindCommand.predicates); } @Override public String toString() { return new ToStringBuilder(this) - .add("predicate", predicate) + .add("predicates", predicates) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..c74bc5fee7b 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -8,14 +8,25 @@ public class HelpCommand extends Command { public static final String COMMAND_WORD = "help"; + public static final CommandType COMMAND_TYPE = CommandType.HELP; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" + "Example: " + COMMAND_WORD; public static final String SHOWING_HELP_MESSAGE = "Opened help window."; + /** + * Returns Command Type HELP + * + * @return Command Type HELP + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult(SHOWING_HELP_MESSAGE, COMMAND_TYPE); } } diff --git a/src/main/java/seedu/address/logic/commands/ImportCommand.java b/src/main/java/seedu/address/logic/commands/ImportCommand.java new file mode 100644 index 00000000000..af52f3a8b59 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ImportCommand.java @@ -0,0 +1,264 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.course.Course; +import seedu.address.model.student.Email; +import seedu.address.model.student.Name; +import seedu.address.model.student.Phone; +import seedu.address.model.student.Student; + +/** + * Imports students from a CSV file into TAHub. + */ +public class ImportCommand extends Command { + + public static final String COMMAND_WORD = "import"; + public static final CommandType COMMAND_TYPE = CommandType.STUDENT; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Imports students from a CSV file.\n" + + "For files in parent directory: " + COMMAND_WORD + " filename.csv\n" + + "For files in home directory: " + COMMAND_WORD + " ~/path/to/file.csv\n" + + "Examples:\n" + + " " + COMMAND_WORD + " students.csv\n" + + " " + COMMAND_WORD + " ~/documents/students.csv\n" + + " " + COMMAND_WORD + " ~/semester1/class1/students.csv"; + + public static final String MESSAGE_FILE_OUTSIDE_PROJECT = + "The file must be in the parent directory or specified with a complete path from home directory (~)"; + + public static final String MESSAGE_SUCCESS = "Imported %1$d students successfully. %2$d entries had errors."; + public static final String MESSAGE_SUCCESS_WITH_ERRORS = MESSAGE_SUCCESS + " Failed entries written to: %3$s"; + public static final String MESSAGE_EMPTY_FILE = "The specified file is empty or contains no valid entries"; + public static final String MESSAGE_INVALID_FILE = "Could not read the specified file: %1$s"; + public static final String MESSAGE_INVALID_HEADER = "Invalid CSV header. Expected: Name,Phone,Email,Courses"; + + private static final Logger logger = LogsCenter.getLogger(ImportCommand.class); + private final Path filePath; + private int successCount = 0; + private int errorCount = 0; + + /** + * Creates an ImportCommand to import data from the specified path into TAHub + */ + public ImportCommand(String filepath) { + requireAllNonNull(filepath); + this.filePath = resolveFilePath(filepath); + } + + /** + * Returns Command Type IMPORTSTUDENT + * + * @return Command Type IMPORTSTUDENT + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireAllNonNull(model); + List errorEntries = new ArrayList<>(); + + try (BufferedReader reader = new BufferedReader(new FileReader(filePath.toFile()))) { + String header = reader.readLine(); + if (header == null) { + throw new CommandException(MESSAGE_EMPTY_FILE); + } + + // Validate header + if (!header.equalsIgnoreCase("Name,Phone,Email,Courses")) { + throw new CommandException(MESSAGE_INVALID_HEADER); + } + + String line; + boolean hasValidEntries = false; + while ((line = reader.readLine()) != null) { + if (line.trim().isEmpty()) { + continue; + } + hasValidEntries = true; + try { + Student student = parseStudent(line); + if (!model.hasStudent(student)) { + model.addStudent(student); + successCount++; + logger.fine("Successfully imported student: " + student.getName()); + } else { + errorEntries.add(new String[]{line, "Duplicate student"}); + errorCount++; + logger.fine("Duplicate student found: " + student.getName()); + } + } catch (IllegalArgumentException e) { + errorEntries.add(new String[]{line, e.getMessage()}); + errorCount++; + logger.fine("Error parsing student entry: " + e.getMessage()); + } + } + + if (!hasValidEntries) { + throw new CommandException(MESSAGE_EMPTY_FILE); + } + + if (!errorEntries.isEmpty()) { + Path errorPath = writeErrorFile(errorEntries); + return new CommandResult( + String.format(MESSAGE_SUCCESS_WITH_ERRORS, successCount, errorCount, errorPath), + COMMAND_TYPE); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, successCount, errorCount), COMMAND_TYPE); + + } catch (IOException e) { + logger.warning("Error reading import file: " + e.getMessage()); + throw new CommandException(String.format(MESSAGE_INVALID_FILE, e.getMessage())); + } + } + + /** + * Resolves the file path, handling different possible locations in priority order: + * 1. Data directory (./data/) + * 2. Current directory + * 3. Home directory paths (starting with ~) + * 4. Absolute paths + */ + protected Path resolveFilePath(String filepath) { + // Remove .csv extension if present for consistency + String filename = filepath.endsWith(".csv") + ? filepath.substring(0, filepath.length() - 4) + : filepath; + + // Check data directory first + Path dataPath = Paths.get("data", filename + ".csv"); + if (Files.exists(dataPath)) { + return dataPath; + } + + // Then try the direct path in case it's in current directory + Path directPath = Paths.get(filepath); + if (Files.exists(directPath)) { + return directPath; + } + + // If path starts with ~, expand to user home directory + if (filepath.startsWith("~")) { + return Paths.get(System.getProperty("user.home")) + .resolve(filepath.substring(2)); + } + + // If nothing found, default to data directory path for error message consistency + return dataPath; + } + + /** + * Writes failed entries to an error file in the same directory as the input file. + */ + private Path writeErrorFile(List errorEntries) throws IOException { + Path errorPath = filePath.resolveSibling("error.csv"); + try (FileWriter writer = new FileWriter(errorPath.toFile())) { + writer.write("Original Entry,Error Message\n"); + for (String[] entry : errorEntries) { + writer.write(String.format("%s,%s\n", + escapeSpecialCharacters(entry[0]), + escapeSpecialCharacters(entry[1]))); + } + } + return errorPath; + } + + /** + * Parses a CSV line into a Student object. + */ + protected Student parseStudent(String line) throws IllegalArgumentException { + String[] parts = line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1); + if (parts.length < 4) { + throw new IllegalArgumentException("Incomplete student entry"); + } + + try { + String name = unescapeSpecialCharacters(parts[0]); + String phone = parts[1].trim(); + String email = parts[2].trim(); + String coursesStr = unescapeSpecialCharacters(parts[3]); + + Set courses = new HashSet<>(); + if (!coursesStr.isEmpty()) { + for (String course : coursesStr.split(";")) { + courses.add(new Course(course.trim())); + } + } + + return new Student( + new Name(name), + new Phone(phone), + new Email(email), + courses + ); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid data format: " + e.getMessage()); + } + } + + /** + * Escapes special characters in CSV data. + * Made public for testing. + */ + public String escapeSpecialCharacters(String data) { + if (data == null) { + return ""; + } + String escapedData = data.replaceAll("\\R", " "); + if (data.contains(",") || data.contains("\"") || data.contains("'")) { + data = data.replace("\"", "\"\""); + escapedData = "\"" + data + "\""; + } + return escapedData; + } + + /** + * Unescapes special characters in CSV data. + * Made public for testing. + */ + public String unescapeSpecialCharacters(String data) { + if (data == null) { + return ""; + } + data = data.trim(); + if (data.startsWith("\"") && data.endsWith("\"")) { + data = data.substring(1, data.length() - 1); + data = data.replace("\"\"", "\""); + } + return data; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ImportCommand)) { + return false; + } + + ImportCommand otherCommand = (ImportCommand) other; + return filePath.equals(otherCommand.filePath); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..8c431afec88 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -1,24 +1,33 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_STUDENTS; import seedu.address.model.Model; /** - * Lists all persons in the address book to the user. + * Lists all students 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"; + public static final String COMMAND_WORD = "liststudents"; + public static final CommandType COMMAND_TYPE = CommandType.STUDENT; + public static final String MESSAGE_SUCCESS = "Listed all students"; + /** + * Returns Command Type LIST + * + * @return Command Type LIST + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } @Override public CommandResult execute(Model model) { requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); + model.updateFilteredStudentList(PREDICATE_SHOW_ALL_STUDENTS); + return new CommandResult(MESSAGE_SUCCESS, COMMAND_TYPE); } } diff --git a/src/main/java/seedu/address/logic/commands/consultation/AddConsultCommand.java b/src/main/java/seedu/address/logic/commands/consultation/AddConsultCommand.java new file mode 100644 index 00000000000..41b47dcaafc --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/consultation/AddConsultCommand.java @@ -0,0 +1,91 @@ +package seedu.address.logic.commands.consultation; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TIME; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.consultation.Consultation; + +/** + * Adds a student to the address book. + */ +public class AddConsultCommand extends Command { + + public static final String COMMAND_WORD = "addconsult"; + public static final CommandType COMMAND_TYPE = CommandType.CONSULT; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a consultation to TAHub. " + + "Parameters: " + + PREFIX_DATE + "DATE " + + PREFIX_TIME + "TIME " + + "Example: " + COMMAND_WORD + " " + + PREFIX_DATE + "2024-10-20 " + + PREFIX_TIME + "14:00 "; + + public static final String MESSAGE_SUCCESS = "New consult added: %1$s"; + + private final Consultation newConsult; + + /** + * Creates an AddCommand to add the specified {@code Student} + */ + public AddConsultCommand(Consultation newConsult) { + requireNonNull(newConsult); + this.newConsult = newConsult; + } + + /** + * Returns Command Type ADDCONSULT + * + * @return Command Type ADDCONSULT + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + // Check if the model already has a consultation with the same date and time + if (model.hasConsult(newConsult)) { + throw new CommandException("Duplicate consultation." + + " A consultation with this date and time already exists."); + } + + // Add the consultation if it's unique + model.addConsult(newConsult); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(newConsult)), + COMMAND_TYPE); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddConsultCommand)) { + return false; + } + + AddConsultCommand otherAddCommand = (AddConsultCommand) other; + return newConsult.equals(otherAddCommand.newConsult); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("newConsult", newConsult) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/consultation/AddToConsultCommand.java b/src/main/java/seedu/address/logic/commands/consultation/AddToConsultCommand.java new file mode 100644 index 00000000000..95f6e20e170 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/consultation/AddToConsultCommand.java @@ -0,0 +1,184 @@ +package seedu.address.logic.commands.consultation; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_CONSULTATION_DISPLAYED_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.consultation.Consultation; +import seedu.address.model.student.Name; +import seedu.address.model.student.Student; + +/** + * Adds students to a specific consultation. + */ +public class AddToConsultCommand extends Command { + + public static final String COMMAND_WORD = "addtoconsult"; + public static final CommandType COMMAND_TYPE = CommandType.CONSULT; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds students to the consultation identified " + + "by the index number used in the displayed consultation list. " + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME]… [" + PREFIX_INDEX + "INDEX]…\n" + + "Example: " + COMMAND_WORD + " 1 " + PREFIX_NAME + "John Doe " + PREFIX_INDEX + "1"; + + public static final String MESSAGE_ADD_TO_CONSULT_SUCCESS = "Added students to the Consultation: %1$s"; + + public static final String MESSAGE_STUDENT_NOT_FOUND = "Student(s) %s not found"; + public static final String MESSAGE_DUPLICATE_STUDENT_IN_CONSULTATION_BY_NAME = + "%s is already added to the consultation!"; + public static final String MESSAGE_DUPLICATE_STUDENT_IN_CONSULTATION_BY_INDEX = + "%s at index %d is already added to the consultation!"; + private final Logger logger = LogsCenter.getLogger(AddToConsultCommand.class); + + + + private final Index index; + private final List studentNames; + private final List indices; + + /** + * @param index of the consultation in the filtered consultation list + * @param studentNames list of student names to add to the consultation + * @param indices list of indices of students in the current filtered list to add to the current consultation + */ + public AddToConsultCommand(Index index, List studentNames, List indices) { + requireNonNull(index); + requireNonNull(studentNames); + requireNonNull(indices); + + + this.index = index; + this.studentNames = studentNames; + this.indices = indices; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredConsultationList(); + List lastShownStudentList = model.getFilteredStudentList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(String.format(MESSAGE_INVALID_CONSULTATION_DISPLAYED_INDEX, + index.getOneBased())); + } + + Consultation targetConsultation = lastShownList.get(index.getZeroBased()); + Consultation editedConsultation = new Consultation(targetConsultation); + + Set studentsToAddSet = new HashSet<>(); + ArrayList studentsToAddArr = new ArrayList<>(); + + for (Name studentName : studentNames) { + Student student = model.findStudentByName(studentName) + .orElseThrow(() -> new CommandException( + String.format(MESSAGE_STUDENT_NOT_FOUND, studentName))); + + // if student was already in the lesson before this command, throw error + if (editedConsultation.hasStudent(student)) { + logger.warning("Students were not added to consult " + targetConsultation.toString() + + " because the student " + studentName + " was already in consult"); + throw new CommandException( + String.format(MESSAGE_DUPLICATE_STUDENT_IN_CONSULTATION_BY_NAME, studentName)); + } + + //if student was not previously in the lesson before this command, add it to the set + if (!studentsToAddSet.contains(student)) { + studentsToAddArr.add(student); + } + studentsToAddSet.add(student); + } + + // check if any of the indices are out of bounds of the filtered student list + boolean throwException = false; + Set outOfBounds = new HashSet<>(); + + for (Index item: indices) { + if (item.getZeroBased() >= lastShownStudentList.size()) { + throwException = true; + outOfBounds.add(item); + } + } + + if (throwException) { + + String formattedOutOfBoundIndices = outOfBounds.stream() + .map(index -> String.valueOf(index.getOneBased())) + .collect(Collectors.joining(", ")); + logger.warning("Students were not added to consult " + targetConsultation.toString() + + " because indices " + formattedOutOfBoundIndices + " were out of bounds"); + throw new CommandException(String.format(Messages.MESSAGE_INVALID_INDEX_SHOWN, formattedOutOfBoundIndices)); + } + + // Make sure that there are no duplicate students. Since logic handling duplicate students is done here for + // adding students by name, logic for handling duplicate indices are also handled here + for (Index studentIndex : indices) { + Student student = lastShownStudentList.get(studentIndex.getZeroBased()); + + if (editedConsultation.hasStudent(student)) { + logger.warning("Students were not added to lesson " + targetConsultation.toString() + + " because student " + student.getName() + " was already in the consult"); + throw new CommandException( + String.format(MESSAGE_DUPLICATE_STUDENT_IN_CONSULTATION_BY_INDEX, student.getName(), + studentIndex.getOneBased())); + } + + if (!studentsToAddSet.contains(student)) { + studentsToAddArr.add(student); + } + studentsToAddSet.add(student); + } + + // actually adding the students into the lesson + for (Student student: studentsToAddArr) { + editedConsultation.addStudent(student); + } + model.setConsult(targetConsultation, editedConsultation); + + return new CommandResult( + String.format(MESSAGE_ADD_TO_CONSULT_SUCCESS, Messages.format(editedConsultation)), + COMMAND_TYPE); + } + + /** + * Returns Command Type ADDTOCONSULT + * + * @return Command Type ADDTOCONSULT + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AddToConsultCommand)) { + return false; + } + + AddToConsultCommand otherCommand = (AddToConsultCommand) other; + return index.equals(otherCommand.index) + && studentNames.equals(otherCommand.studentNames); + } +} diff --git a/src/main/java/seedu/address/logic/commands/consultation/DeleteConsultCommand.java b/src/main/java/seedu/address/logic/commands/consultation/DeleteConsultCommand.java new file mode 100644 index 00000000000..b0f7269f3ff --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/consultation/DeleteConsultCommand.java @@ -0,0 +1,112 @@ +package seedu.address.logic.commands.consultation; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.DEFAULT_DELIMITER; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.consultation.Consultation; + +/** + * Deletes a consultation by its index in TAHub. + */ +public class DeleteConsultCommand extends Command { + + public static final String COMMAND_WORD = "deleteconsult"; + public static final CommandType COMMAND_TYPE = CommandType.CONSULT; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the consultation identified by the index number used in the displayed consultation list.\n" + + "Parameters: INDEX (must be a positive integer) [;INDEX…]\n" + + "Example: " + COMMAND_WORD + " 1" + DEFAULT_DELIMITER + "2"; + + public static final String MESSAGE_DELETE_CONSULT_SUCCESS = "Deleted Consult(s):\n%1$s"; + + private final Set targetIndices; + + public DeleteConsultCommand(Set targetIndices) { + this.targetIndices = targetIndices; + } + + /** + * Returns Command Type DELETECONSULT + * + * @return Command Type DELETECONSULT + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List consults = model.getFilteredConsultationList(); + + boolean throwException = false; + ArrayList outOfBounds = new ArrayList<>(); + + for (Index item: targetIndices) { + if (item.getZeroBased() >= consults.size()) { + throwException = true; + outOfBounds.add(item); + } + } + + if (throwException) { + String formattedOutOfBoundIndices = outOfBounds.stream() + .map(index -> String.valueOf(index.getOneBased())) + .collect(Collectors.joining(", ")); + throw new CommandException(String.format(Messages.MESSAGE_INVALID_INDEX_SHOWN, formattedOutOfBoundIndices)); + } + + List deletedPeople = targetIndices.stream() + .map(targetIndex -> consults.get(targetIndex.getZeroBased())) + .toList(); + + + deletedPeople.forEach(model::deleteConsult); + + String formattedDeletedPeople = deletedPeople.stream() + .map(Messages::format) + .collect(Collectors.joining("\n")); + + + return new CommandResult( + String.format(MESSAGE_DELETE_CONSULT_SUCCESS, formattedDeletedPeople), + COMMAND_TYPE); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteConsultCommand)) { + return false; + } + + DeleteConsultCommand otherDeleteCommand = (DeleteConsultCommand) other; + return targetIndices.equals(otherDeleteCommand.targetIndices); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndices", targetIndices) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/consultation/ExportConsultCommand.java b/src/main/java/seedu/address/logic/commands/consultation/ExportConsultCommand.java new file mode 100644 index 00000000000..64651582ef3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/consultation/ExportConsultCommand.java @@ -0,0 +1,178 @@ +package seedu.address.logic.commands.consultation; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.consultation.Consultation; + +/** + * Exports the currently displayed list of consultations to a CSV file. + */ +public class ExportConsultCommand extends Command { + + public static final String COMMAND_WORD = "exportconsult"; + public static final String FORCE_FLAG = "-f"; + public static final CommandType COMMAND_TYPE = CommandType.CONSULT; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Exports the current list of consultations to a CSV file. " + + "Parameters: FILENAME " + "[" + FORCE_FLAG + "] " + + "\nExample: " + COMMAND_WORD + " consultations" + + "\nExample with force flag: " + COMMAND_WORD + " " + FORCE_FLAG + " consultations"; + + public static final String MESSAGE_FAILURE = "Failed to export consultations: %1$s"; + public static final String MESSAGE_FILE_EXISTS = "File %1$s already exists. Use -f flag to overwrite."; + public static final String MESSAGE_HOME_FILE_EXISTS = + "File %1$s already exists in home directory. Use -f flag to overwrite."; + public static final String MESSAGE_SUCCESS = "Exported %1$d consultations to %2$s"; + public static final String MESSAGE_SUCCESS_WITH_COPY = "Exported %1$d consultations to %2$s and %3$s"; + public static final String INVALID_FILENAME_MESSAGE = + "Filename can only contain alphanumeric characters (A-Z, a-z, 0-9)"; + + private static final Logger logger = LogsCenter.getLogger(ExportConsultCommand.class); + + private final String filename; + private final boolean isForceExport; + private final Path baseDir; + + public ExportConsultCommand(String filename, boolean isForceExport) { + this(filename, isForceExport, Paths.get("data")); + } + + /** + * Creates an ExportConsultCommand to export data to the specified filename in the default directory + */ + public ExportConsultCommand(String filename, boolean isForceExport, Path baseDir) { + this.filename = filename; + this.isForceExport = isForceExport; + this.baseDir = baseDir; + } + + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + protected Path getHomeFilePath(String filename) { + return Paths.get(System.getProperty("user.home"), filename + ".csv"); + } + + /** + * Validates if filename is valid + * + * @param filename String representing filename to be validated + */ + protected void validateFilename(String filename) throws CommandException { + // Check if filename contains any non-alphanumeric characters + if (!filename.matches("^[a-zA-Z0-9]+$")) { + throw new CommandException(ExportConsultCommand.INVALID_FILENAME_MESSAGE); + } + } + + @Override + public CommandResult execute(Model model) throws CommandException { + validateFilename(filename); + List consultList = model.getFilteredConsultationList(); + logger.info("Starting export for " + consultList.size() + " consultations"); + + Path dataFilePath = baseDir.resolve(filename + ".csv"); + Path homeFilePath = getHomeFilePath(filename); + + try { + Files.createDirectories(baseDir); + } catch (IOException e) { + logger.warning("Failed to create directory: " + baseDir); + throw new CommandException(String.format(MESSAGE_FAILURE, "Could not create directory: " + e.getMessage())); + } + + if (!isForceExport) { + if (Files.exists(dataFilePath)) { + throw new CommandException(String.format(MESSAGE_FILE_EXISTS, dataFilePath)); + } + if (Files.exists(homeFilePath)) { + throw new CommandException(String.format(MESSAGE_HOME_FILE_EXISTS, homeFilePath)); + } + } + + try { + writeCsvFile(dataFilePath, consultList); + + try { + if (isForceExport) { + Files.copy(dataFilePath, homeFilePath, StandardCopyOption.REPLACE_EXISTING); + } else { + Files.copy(dataFilePath, homeFilePath); + } + return new CommandResult(String.format(MESSAGE_SUCCESS_WITH_COPY, + consultList.size(), dataFilePath, homeFilePath), + COMMAND_TYPE); + } catch (IOException e) { + return new CommandResult(String.format(MESSAGE_SUCCESS, consultList.size(), dataFilePath), + COMMAND_TYPE); + } + } catch (IOException e) { + try { + Files.deleteIfExists(dataFilePath); + } catch (IOException ignored) { + // Ignore cleanup errors + } + throw new CommandException(String.format(MESSAGE_FAILURE, e.getMessage())); + } + } + + String escapeSpecialCharacters(String data) { + String escapedData = data.replaceAll("\\R", " "); + if (data.contains(",") || data.contains("\"") || data.contains("'")) { + data = data.replace("\"", "\"\""); + escapedData = "\"" + data + "\""; + } + return escapedData; + } + + private void writeCsvFile(Path filePath, List consultList) throws IOException { + try (FileWriter csvWriter = new FileWriter(filePath.toFile())) { + // Write CSV header + csvWriter.append("Date,Time,Students\n"); + + // Write consultation data + for (Consultation consult : consultList) { + List studentNames = consult.getStudents().stream() + .map(student -> student.getName().fullName) + .collect(Collectors.toList()); + String studentsString = String.join(";", studentNames); + + csvWriter.append(String.format("%s,%s,%s\n", + consult.getDate().getValue(), + consult.getTime().getValue(), + escapeSpecialCharacters(studentsString))); + } + csvWriter.flush(); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof ExportConsultCommand)) { + return false; + } + ExportConsultCommand otherCommand = (ExportConsultCommand) other; + return filename.equals(otherCommand.filename) + && isForceExport == otherCommand.isForceExport; + } +} diff --git a/src/main/java/seedu/address/logic/commands/consultation/ImportConsultCommand.java b/src/main/java/seedu/address/logic/commands/consultation/ImportConsultCommand.java new file mode 100644 index 00000000000..23d0bcbff10 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/consultation/ImportConsultCommand.java @@ -0,0 +1,290 @@ +package seedu.address.logic.commands.consultation; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.consultation.Consultation; +import seedu.address.model.datetime.Date; +import seedu.address.model.datetime.Time; +import seedu.address.model.student.Name; +import seedu.address.model.student.Student; + +/** + * Imports consultations from a CSV file into TAHub. + */ +public class ImportConsultCommand extends Command { + + public static final String COMMAND_WORD = "importconsult"; + public static final CommandType COMMAND_TYPE = CommandType.CONSULT; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Imports consultations from a CSV file.\n" + + "For files in parent directory: " + COMMAND_WORD + " filename.csv\n" + + "For files in home directory: " + COMMAND_WORD + " ~/path/to/file.csv\n" + + "Examples:\n" + + " " + COMMAND_WORD + " consultations.csv\n" + + " " + COMMAND_WORD + " ~/documents/consultations.csv\n" + + " " + COMMAND_WORD + " ~/semester1/consultations.csv"; + + public static final String MESSAGE_FILE_OUTSIDE_PROJECT = + "The file must be in the parent directory or specified with a complete path from home directory (~)"; + + public static final String MESSAGE_SUCCESS = "Imported %1$d consultations successfully. %2$d entries had errors."; + public static final String MESSAGE_SUCCESS_WITH_ERRORS = MESSAGE_SUCCESS + " Failed entries written to: %3$s"; + public static final String MESSAGE_EMPTY_FILE = "The specified file is empty or contains no valid entries"; + public static final String MESSAGE_INVALID_FILE = "Could not read the specified file: %1$s"; + public static final String MESSAGE_INVALID_HEADER = "Invalid CSV header. Expected: Date,Time,Students"; + public static final String MESSAGE_STUDENT_NOT_FOUND = "Student '%s' not found in the system"; + + private static final Logger logger = LogsCenter.getLogger(ImportConsultCommand.class); + private final Path filePath; + private int successCount = 0; + private int errorCount = 0; + + /** + * Creates an ImportConsultCommand to import consultation data from the specified path + */ + public ImportConsultCommand(String filepath) { + requireAllNonNull(filepath); + this.filePath = resolveFilePath(filepath); + } + + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireAllNonNull(model); + List errorEntries = new ArrayList<>(); + + try (BufferedReader reader = new BufferedReader(new FileReader(filePath.toFile()))) { + String header = reader.readLine(); + if (header == null) { + throw new CommandException(MESSAGE_EMPTY_FILE); + } + + if (!header.equalsIgnoreCase("Date,Time,Students")) { + throw new CommandException(MESSAGE_INVALID_HEADER); + } + + boolean hasValidEntries = parseFileEntries(errorEntries, reader, model); + + if (!hasValidEntries) { + throw new CommandException(MESSAGE_EMPTY_FILE); + } + + if (!errorEntries.isEmpty()) { + Path errorPath = writeErrorFile(errorEntries); + return new CommandResult( + String.format(MESSAGE_SUCCESS_WITH_ERRORS, successCount, errorCount, errorPath), + COMMAND_TYPE); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, successCount, errorCount), COMMAND_TYPE); + + } catch (IOException e) { + logger.warning("Error reading import file: " + e.getMessage()); + throw new CommandException(String.format(MESSAGE_INVALID_FILE, e.getMessage())); + } + } + + /** + * Helper function to parse a .csv file + * @param errorEntries A List to be populated with error entries + * @param reader A BufferedReader instance to read the file + * @return A boolean representing whether the file had valid entries + */ + private boolean parseFileEntries(List errorEntries, + BufferedReader reader, + Model model) throws IOException { + String line; + boolean hasValidEntries = false; + while ((line = reader.readLine()) != null) { + if (line.trim().isEmpty()) { + continue; + } + hasValidEntries = true; + try { + Consultation consultation = parseConsultation(line, model); + if (!model.hasConsult(consultation)) { + model.addConsult(consultation); + successCount++; + logger.fine("Successfully imported consultation: " + consultation); + } else { + errorEntries.add(new String[]{line, "Duplicate consultation"}); + errorCount++; + logger.fine("Duplicate consultation found: " + consultation); + } + } catch (IllegalArgumentException e) { + errorEntries.add(new String[]{line, e.getMessage()}); + errorCount++; + logger.fine("Error parsing consultation entry: " + e.getMessage()); + } + } + return hasValidEntries; + } + + /** + * Resolves the file path, handling different possible locations in priority order: + * 1. Data directory (./data/) + * 2. Current directory + * 3. Home directory paths (starting with ~) + * 4. Absolute paths + */ + protected Path resolveFilePath(String filepath) { + try { + // Remove .csv extension if present for consistency + String filename = filepath.endsWith(".csv") + ? filepath.substring(0, filepath.length() - 4) + : filepath; + + // Check data directory first + Path dataPath = Paths.get("data").resolve(filename + ".csv").normalize(); + if (Files.exists(dataPath)) { + return dataPath; + } + + // Then try the direct path in case it's in current directory + Path directPath = Paths.get(filename + ".csv").normalize(); + if (Files.exists(directPath)) { + return directPath; + } + + // If path starts with ~, expand to user home directory + if (filename.startsWith("~")) { + return Paths.get(System.getProperty("user.home")) + .resolve(filename.substring(2) + ".csv") + .normalize(); + } + + // For absolute paths, try to handle them directly + if (Paths.get(filepath).isAbsolute()) { + return Paths.get(filepath).normalize(); + } + + // If nothing found, default to data directory path for error message consistency + return dataPath; + } catch (InvalidPathException e) { + // If we get an invalid path, fall back to treating it as a simple filename in data directory + return Paths.get("data", filepath).normalize(); + } + } + + private Path writeErrorFile(List errorEntries) throws IOException { + Path errorPath = filePath.resolveSibling("error.csv"); + try (FileWriter writer = new FileWriter(errorPath.toFile())) { + writer.write("Original Entry,Error Message\n"); + for (String[] entry : errorEntries) { + writer.write(String.format("%s,%s\n", + escapeSpecialCharacters(entry[0]), + escapeSpecialCharacters(entry[1]))); + } + } + return errorPath; + } + + /** + * Parses a CSV line into a Consultation object. + */ + protected Consultation parseConsultation(String line, Model model) throws IllegalArgumentException { + String[] parts = line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1); + if (parts.length < 3) { + throw new IllegalArgumentException("Incomplete consultation entry"); + } + + try { + String date = parts[0].trim(); + String time = parts[1].trim(); + String studentsStr = unescapeSpecialCharacters(parts[2]); + + List students = getStudentsFromString(studentsStr, model); + + return new Consultation(new Date(date), new Time(time), students); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid data format: " + e.getMessage()); + } + } + + /** + * Extracts a list of Students from a semicolon-separated string. + */ + private List getStudentsFromString(String studentsStr, Model model) throws IllegalArgumentException { + List students = new ArrayList<>(); + if (!studentsStr.isEmpty()) { + for (String studentName : studentsStr.split(";")) { + String trimmedName = studentName.trim(); + Optional student = model.findStudentByName(new Name(trimmedName)); + if (student.isEmpty()) { + throw new IllegalArgumentException(String.format(MESSAGE_STUDENT_NOT_FOUND, trimmedName)); + } + students.add(student.get()); + } + } + return students; + } + + /** + * Escapes special characters in CSV data. + * Made public for testing. + */ + public String escapeSpecialCharacters(String data) { + if (data == null) { + return ""; + } + String escapedData = data.replaceAll("\\R", " "); + if (data.contains(",") || data.contains("\"") || data.contains("'")) { + data = data.replace("\"", "\"\""); + escapedData = "\"" + data + "\""; + } + return escapedData; + } + + /** + * Unescapes special characters in CSV data. + * Made public for testing. + */ + public String unescapeSpecialCharacters(String data) { + if (data == null) { + return ""; + } + data = data.trim(); + if (data.startsWith("\"") && data.endsWith("\"")) { + data = data.substring(1, data.length() - 1); + data = data.replace("\"\"", "\""); + } + return data; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ImportConsultCommand)) { + return false; + } + + ImportConsultCommand otherCommand = (ImportConsultCommand) other; + return filePath.equals(otherCommand.filePath); + } +} diff --git a/src/main/java/seedu/address/logic/commands/consultation/ListConsultsCommand.java b/src/main/java/seedu/address/logic/commands/consultation/ListConsultsCommand.java new file mode 100644 index 00000000000..40fcaded14d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/consultation/ListConsultsCommand.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands.consultation; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_CONSULTATIONS; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.model.Model; + +/** + * Lists all consultations in the address book to the user. + */ +public class ListConsultsCommand extends Command { + + public static final String COMMAND_WORD = "listconsults"; + public static final CommandType COMMAND_TYPE = CommandType.CONSULT; + public static final String MESSAGE_SUCCESS = "Listed all consultations"; + + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredConsultationList(PREDICATE_SHOW_ALL_CONSULTATIONS); + return new CommandResult(MESSAGE_SUCCESS, COMMAND_TYPE); + } +} diff --git a/src/main/java/seedu/address/logic/commands/consultation/RemoveFromConsultCommand.java b/src/main/java/seedu/address/logic/commands/consultation/RemoveFromConsultCommand.java new file mode 100644 index 00000000000..ee14202c65c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/consultation/RemoveFromConsultCommand.java @@ -0,0 +1,99 @@ +package seedu.address.logic.commands.consultation; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.consultation.Consultation; +import seedu.address.model.student.Name; +import seedu.address.model.student.Student; + +/** + * Removes multiple students from a specific consultation identified by its index. + */ +public class RemoveFromConsultCommand extends Command { + + public static final String COMMAND_WORD = "removefromconsult"; + public static final CommandType COMMAND_TYPE = CommandType.CONSULT; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Removes students from the consultation identified by the index.\n" + + "Parameters: CONSULT_INDEX (must be a positive integer) " + + PREFIX_NAME + "NAME [" + PREFIX_NAME + "NAME]…\n" + + "Example: " + COMMAND_WORD + " 1 n/Alex Yeoh n/Harry Ng"; + + public static final String MESSAGE_REMOVE_FROM_CONSULT_SUCCESS = + "Removed students from Consultation: Date: %s; Time: %s"; + public static final String MESSAGE_STUDENT_NOT_FOUND = "Student(s) not found in the consultation."; + + private final Index consultIndex; + private final List studentNames; + + /** + * @param consultIndex The index of the consultation in the filtered consultation list. + * @param studentNames List of student names to remove from the consultation. + */ + public RemoveFromConsultCommand(Index consultIndex, List studentNames) { + requireNonNull(consultIndex); + requireNonNull(studentNames); + + this.consultIndex = consultIndex; + this.studentNames = studentNames; + } + + /** + * Returns Command Type REMOVEFROMCONSULT + * + * @return Command Type REMOVEFROMCONSULT + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredConsultationList(); + + if (consultIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException("The consultation index provided is invalid."); + } + + Consultation targetConsultation = lastShownList.get(consultIndex.getZeroBased()); + Consultation editedConsultation = new Consultation(targetConsultation); + + for (Name studentName : studentNames) { + Student studentToRemove = model.findStudentByName(studentName) + .orElseThrow(() -> new CommandException("Student not found: " + studentName)); + + if (!editedConsultation.hasStudent(studentToRemove)) { + throw new CommandException(MESSAGE_STUDENT_NOT_FOUND); + } + + editedConsultation.removeStudent(studentToRemove); + } + + model.setConsult(targetConsultation, editedConsultation); + + String successMessage = String.format(MESSAGE_REMOVE_FROM_CONSULT_SUCCESS, + editedConsultation.getDate().getValue(), editedConsultation.getTime().getValue()); + + return new CommandResult(successMessage, COMMAND_TYPE); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof RemoveFromConsultCommand // instanceof handles nulls + && consultIndex.equals(((RemoveFromConsultCommand) other).consultIndex) + && studentNames.equals(((RemoveFromConsultCommand) other).studentNames)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/lesson/AddLessonCommand.java b/src/main/java/seedu/address/logic/commands/lesson/AddLessonCommand.java new file mode 100644 index 00000000000..59f3db09200 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/lesson/AddLessonCommand.java @@ -0,0 +1,80 @@ +package seedu.address.logic.commands.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TIME; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.lesson.exceptions.DuplicateLessonException; + +/** + * Adds a lesson to the address book. + */ +public class AddLessonCommand extends Command { + + public static final String COMMAND_WORD = "addlesson"; + public static final CommandType COMMAND_TYPE = CommandType.LESSON; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a lesson to TAHub. " + + "\nParameters: " + + PREFIX_DATE + "DATE " + + PREFIX_TIME + "TIME " + + "\nExample: " + COMMAND_WORD + " " + + PREFIX_DATE + "2024-10-20 " + + PREFIX_TIME + "14:00 "; + + public static final String MESSAGE_SUCCESS = "New lesson added: %1$s"; + + private final Lesson newLesson; + + /** + * Creates an AddLessonCommand to add the specified {@code Lesson} + */ + public AddLessonCommand(Lesson newLesson) { + requireNonNull(newLesson); + this.newLesson = newLesson; + } + + /** + * Returns Command Type ADDLESSON + * + * @return Command Type ADDLESSON + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + try { + model.addLesson(newLesson); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(newLesson)), COMMAND_TYPE); + } catch (DuplicateLessonException e) { + throw new CommandException("Duplicate lesson. A lesson with this date and time already exists."); + } + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof AddLessonCommand + && newLesson.equals(((AddLessonCommand) other).newLesson)); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("newLesson", newLesson) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/lesson/AddToLessonCommand.java b/src/main/java/seedu/address/logic/commands/lesson/AddToLessonCommand.java new file mode 100644 index 00000000000..287780c3e9a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/lesson/AddToLessonCommand.java @@ -0,0 +1,180 @@ +package seedu.address.logic.commands.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_LESSON_DISPLAYED_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.student.Name; +import seedu.address.model.student.Student; + +/** + * Adds students to a specific lesson. + */ +public class AddToLessonCommand extends Command { + + public static final String COMMAND_WORD = "addtolesson"; + public static final CommandType COMMAND_TYPE = CommandType.LESSON; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds students to the lesson identified " + + "by the index number used in the displayed lesson list." + + "\nAt least one of the optional parameters must be provided." + + "\nParameters: LESSON_INDEX " + + "[" + PREFIX_NAME + "NAME]… " + + "[" + PREFIX_INDEX + "INDEX]… " + + "\nExample: " + COMMAND_WORD + " 1 n/John Doe i/2"; + + public static final String MESSAGE_ADD_TO_LESSON_SUCCESS = "Added students to the Lesson: %1$s"; + public static final String MESSAGE_STUDENT_NOT_FOUND = "Student not found: %s"; + public static final String MESSAGE_DUPLICATE_STUDENT_IN_LESSON_BY_NAME = + "%s is already added to the lesson!"; + public static final String MESSAGE_DUPLICATE_STUDENT_IN_LESSON_BY_INDEX = + "%s at index %d is already added to the lesson!"; + + + private final Index index; + private final List studentNames; + private final List indices; + private final Logger logger = LogsCenter.getLogger(AddToLessonCommand.class); + + /** + * @param index of the lesson in the filtered lesson list + * @param studentNames list of student names to add to the lesson + * @param indices list of indices of students in the current filtered list to add to the lesson + */ + public AddToLessonCommand(Index index, List studentNames, List indices) { + requireNonNull(index); + requireNonNull(studentNames); + requireNonNull(indices); + + this.index = index; + this.studentNames = studentNames; + this.indices = indices; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + logger.info("Adding students to lesson"); + List lastShownLessonList = model.getFilteredLessonList(); + List lastShownStudentList = model.getFilteredStudentList(); + + if (index.getZeroBased() >= lastShownLessonList.size()) { + throw new CommandException(String.format(MESSAGE_INVALID_LESSON_DISPLAYED_INDEX, + index.getOneBased())); + } + + Lesson targetLesson = lastShownLessonList.get(index.getZeroBased()); + Lesson editedLesson = new Lesson(targetLesson); + + logger.info("Lesson to edit: " + targetLesson.toString()); + + Set studentsToAddSet = new HashSet<>(); + ArrayList studentsToAddArr = new ArrayList<>(); + + for (Name studentName : studentNames) { + Student student = model.findStudentByName(studentName) + .orElseThrow(() -> new CommandException(String.format(MESSAGE_STUDENT_NOT_FOUND, studentName))); + + // if student was already in the lesson before this command, throw error + if (editedLesson.hasStudent(student)) { + logger.warning("Students were not added to lesson " + targetLesson.toString() + + " because the student " + studentName + " was already in lesson"); + throw new CommandException( + String.format(MESSAGE_DUPLICATE_STUDENT_IN_LESSON_BY_NAME, studentName)); + } + + //if student was not previously in the lesson before this command, add it to the set + if (!studentsToAddSet.contains(student)) { + studentsToAddArr.add(student); + } + studentsToAddSet.add(student); + } + + // checking if the indices are out of bounds + boolean throwException = false; + Set outOfBounds = new HashSet<>(); + + for (Index item : indices) { + if (item.getZeroBased() >= lastShownStudentList.size()) { + throwException = true; + outOfBounds.add(item); + } + } + + if (throwException) { + String formattedOutOfBoundIndices = outOfBounds.stream() + .map(index -> String.valueOf(index.getOneBased())) + .collect(Collectors.joining(", ")); + logger.warning("Students were not added to lesson " + targetLesson.toString() + + " because indices " + formattedOutOfBoundIndices + " were out of bounds"); + throw new CommandException(String.format(Messages.MESSAGE_INVALID_INDEX_SHOWN, + formattedOutOfBoundIndices)); + } + + // checking if the students at these indices were already in the lesson + for (Index studentIndex : indices) { + Student student = lastShownStudentList.get(studentIndex.getZeroBased()); + + if (editedLesson.hasStudent(student)) { + logger.warning("Students were not added to lesson " + targetLesson.toString() + + " because student " + student.getName() + " was already in the lesson"); + throw new CommandException( + String.format(MESSAGE_DUPLICATE_STUDENT_IN_LESSON_BY_INDEX, student.getName(), + studentIndex.getOneBased())); + } + + if (!studentsToAddSet.contains(student)) { + studentsToAddArr.add(student); + } + studentsToAddSet.add(student); + } + + // actually adding the students into the lesson + for (Student student: studentsToAddArr) { + editedLesson.addStudent(student); + } + + model.setLesson(targetLesson, editedLesson); + logger.fine("Successfully added students to lesson " + targetLesson.toString()); + return new CommandResult( + String.format(MESSAGE_ADD_TO_LESSON_SUCCESS, Messages.format(editedLesson)), + COMMAND_TYPE); + } + + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AddToLessonCommand)) { + return false; + } + + AddToLessonCommand otherCommand = (AddToLessonCommand) other; + return index.equals(otherCommand.index) + && studentNames.equals(otherCommand.studentNames); + } +} diff --git a/src/main/java/seedu/address/logic/commands/lesson/DeleteLessonCommand.java b/src/main/java/seedu/address/logic/commands/lesson/DeleteLessonCommand.java new file mode 100644 index 00000000000..dde40e2af77 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/lesson/DeleteLessonCommand.java @@ -0,0 +1,102 @@ +package seedu.address.logic.commands.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.DEFAULT_DELIMITER; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.lesson.Lesson; + +/** + * Deletes lessons identified by indices in TAHub. + */ +public class DeleteLessonCommand extends Command { + + public static final String COMMAND_WORD = "deletelesson"; + public static final CommandType COMMAND_TYPE = CommandType.LESSON; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the lesson(s) identified by the index number(s) used in the displayed lesson list." + + "\nParameters: LESSON_INDEX[;LESSON_INDEX]…" + + "\nExample: " + COMMAND_WORD + " 1" + DEFAULT_DELIMITER + "2"; + + public static final String MESSAGE_DELETE_LESSON_SUCCESS = "Deleted Lesson(s):\n%1$s"; + + private final Set targetIndices; + + public DeleteLessonCommand(Set targetIndices) { + this.targetIndices = targetIndices; + } + + /** + * Returns Command Type DELETELESSON + * + * @return Command Type DELETELESSON + */ + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lessons = model.getFilteredLessonList(); + + boolean throwException = false; + ArrayList outOfBounds = new ArrayList<>(); + + for (Index item : targetIndices) { + if (item.getZeroBased() >= lessons.size()) { + throwException = true; + outOfBounds.add(item); + } + } + + if (throwException) { + String formattedOutOfBoundIndices = outOfBounds.stream() + .map(index -> String.valueOf(index.getOneBased())) + .collect(Collectors.joining(", ")); + throw new CommandException(String.format(Messages.MESSAGE_INVALID_INDEX_SHOWN, formattedOutOfBoundIndices)); + } + + List deletedLessons = targetIndices.stream() + .map(targetIndex -> lessons.get(targetIndex.getZeroBased())) + .toList(); + + deletedLessons.forEach(model::deleteLesson); + + String formattedDeletedLessons = deletedLessons.stream() + .map(Messages::format) + .collect(Collectors.joining("\n")); + + return new CommandResult( + String.format(MESSAGE_DELETE_LESSON_SUCCESS, formattedDeletedLessons), + COMMAND_TYPE); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof DeleteLessonCommand + && targetIndices.equals(((DeleteLessonCommand) other).targetIndices)); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndices", targetIndices) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/lesson/ListLessonsCommand.java b/src/main/java/seedu/address/logic/commands/lesson/ListLessonsCommand.java new file mode 100644 index 00000000000..48bda16ec6d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/lesson/ListLessonsCommand.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_LESSONS; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.model.Model; + +/** + * Lists all lessons in the address book to the user. + */ +public class ListLessonsCommand extends Command { + + public static final String COMMAND_WORD = "listlessons"; + public static final CommandType COMMAND_TYPE = CommandType.LESSON; + public static final String MESSAGE_SUCCESS = "Listed all lessons"; + + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredLessonList(PREDICATE_SHOW_ALL_LESSONS); + return new CommandResult(MESSAGE_SUCCESS, COMMAND_TYPE); + } +} diff --git a/src/main/java/seedu/address/logic/commands/lesson/MarkLessonAttendanceCommand.java b/src/main/java/seedu/address/logic/commands/lesson/MarkLessonAttendanceCommand.java new file mode 100644 index 00000000000..30a16b3c6d6 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/lesson/MarkLessonAttendanceCommand.java @@ -0,0 +1,113 @@ +package seedu.address.logic.commands.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_LESSON_DISPLAYED_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ATTENDANCE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.List; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.student.Name; +import seedu.address.model.student.Student; +import seedu.address.model.student.exceptions.StudentNotFoundException; + +/** + * Marks chosen students' attendance for a particular Lesson. + */ +public class MarkLessonAttendanceCommand extends Command { + + public static final String COMMAND_WORD = "marka"; + public static final CommandType COMMAND_TYPE = CommandType.LESSON; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sets the attendance of student(s) " + + "in a lesson at the chosen index in the lesson list to the specified value. " + + "\nParameters: LESSON_INDEX " + + PREFIX_NAME + "NAME [" + PREFIX_NAME + "NAME]… " + + PREFIX_ATTENDANCE + "ATTENDANCE (1/y/Y or 0/n/N) " + + "\nExample: " + COMMAND_WORD + " 1 n/John Doe n/Jane Doe a/y"; + + public static final String MESSAGE_SUCCESS = "Marked the attendance of %s as %s"; + public static final String MESSAGE_STUDENT_NOT_FOUND_IN_ADDRESS_BOOK = "Student not found in TAHub: %s"; + public static final String MESSAGE_STUDENT_NOT_FOUND_IN_LESSON = "Student not found in the lesson: %s"; + + private final Index index; + private final List studentNames; + private final boolean attendance; + private final Logger logger = LogsCenter.getLogger(MarkLessonAttendanceCommand.class); + + /** + * @param index of the lesson in the filtered lesson list + * @param studentNames list of student names to target + * @param attendance true if the given students have attended the lesson, false otherwise + */ + public MarkLessonAttendanceCommand(Index index, List studentNames, boolean attendance) { + requireAllNonNull(index, studentNames); + this.index = index; + this.studentNames = studentNames; + this.attendance = attendance; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + logger.info("Setting lesson attendance of students"); + List lastShownLessonList = model.getFilteredLessonList(); + + if (index.getZeroBased() >= lastShownLessonList.size()) { + throw new CommandException(String.format(MESSAGE_INVALID_LESSON_DISPLAYED_INDEX, index.getOneBased())); + } + + Lesson targetLesson = lastShownLessonList.get(index.getZeroBased()); + Lesson newLesson = new Lesson(targetLesson); + + for (Name studentName : studentNames) { + Student student = model.findStudentByName(studentName) + .orElseThrow(() -> new CommandException( + String.format(MESSAGE_STUDENT_NOT_FOUND_IN_ADDRESS_BOOK, studentName))); + try { + newLesson.setAttendance(student, attendance); + } catch (StudentNotFoundException e) { + throw new CommandException(String.format(MESSAGE_STUDENT_NOT_FOUND_IN_LESSON, studentName)); + } + } + + model.setLesson(targetLesson, newLesson); + + logger.fine("Successfully marked attendance of students in lesson " + targetLesson.toString()); + String names = String.join(", ", studentNames.stream().map(x -> x.fullName).toList()); + return new CommandResult( + String.format(MESSAGE_SUCCESS, names, attendance ? "true" : "false"), + COMMAND_TYPE); + } + + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof MarkLessonAttendanceCommand)) { + return false; + } + + MarkLessonAttendanceCommand otherCommand = (MarkLessonAttendanceCommand) other; + return index.equals(otherCommand.index) + && studentNames.equals(otherCommand.studentNames) + && (attendance == otherCommand.attendance); + } +} diff --git a/src/main/java/seedu/address/logic/commands/lesson/MarkLessonParticipationCommand.java b/src/main/java/seedu/address/logic/commands/lesson/MarkLessonParticipationCommand.java new file mode 100644 index 00000000000..5f0c61b0534 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/lesson/MarkLessonParticipationCommand.java @@ -0,0 +1,129 @@ +package seedu.address.logic.commands.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_LESSON_DISPLAYED_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POINTS; + +import java.util.List; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.student.Name; +import seedu.address.model.student.Student; +import seedu.address.model.student.exceptions.StudentNotFoundException; + +/** + * Marks chosen students' participation for a particular Lesson. + */ +public class MarkLessonParticipationCommand extends Command { + + public static final String COMMAND_WORD = "markp"; + public static final CommandType COMMAND_TYPE = CommandType.LESSON; + public static final int LOWER_BOUND = 0; + public static final int UPPER_BOUND = 100; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sets the participation of student(s) " + + "in a lesson at the chosen index in the lesson list to the specified value. " + + "\nIf their participation is set to a positive integer, also sets their attendance to true." + + "\nParameters: LESSON_INDEX " + + PREFIX_NAME + "NAME [" + PREFIX_NAME + "NAME]… " + + PREFIX_POINTS + "PARTICIPATION (integer from 0-100)" + + "\nExample: " + COMMAND_WORD + " 1 n/John Doe pt/1"; + + public static final String MESSAGE_SUCCESS = "Marked the participation of %s as %s"; + public static final String MESSAGE_STUDENT_NOT_FOUND_IN_ADDRESS_BOOK = "Student not found in TAHub: %s"; + public static final String MESSAGE_STUDENT_NOT_FOUND_IN_LESSON = "Student not found in the lesson: %s"; + + private final Index index; + private final List studentNames; + private final int participationScore; + private final Logger logger = LogsCenter.getLogger(MarkLessonParticipationCommand.class); + + /** + * @param index of the lesson in the filtered lesson list + * @param studentNames list of student names to target + * @param participationScore the student(s)' participation score + */ + public MarkLessonParticipationCommand(Index index, List studentNames, int participationScore) { + requireAllNonNull(index, studentNames); + this.index = index; + this.studentNames = studentNames; + this.participationScore = participationScore; + } + + /** + * Returns true if the participation score is valid, defined as being within the bounds. + */ + public static boolean isValidParticipation(int score) { + return LOWER_BOUND <= score && score <= UPPER_BOUND; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + logger.info("Setting lesson participation of students"); + List lastShownLessonList = model.getFilteredLessonList(); + + if (index.getZeroBased() >= lastShownLessonList.size()) { + throw new CommandException(String.format(MESSAGE_INVALID_LESSON_DISPLAYED_INDEX, index.getOneBased())); + } + + // this should have been checked in the parser + assert MarkLessonParticipationCommand.isValidParticipation(participationScore); + + Lesson targetLesson = lastShownLessonList.get(index.getZeroBased()); + Lesson newLesson = new Lesson(targetLesson); + + for (Name studentName : studentNames) { + Student student = model.findStudentByName(studentName) + .orElseThrow(() -> new CommandException( + String.format(MESSAGE_STUDENT_NOT_FOUND_IN_ADDRESS_BOOK, studentName))); + try { + newLesson.setParticipation(student, participationScore); + if (participationScore > 0) { + newLesson.setAttendance(student, true); + } + } catch (StudentNotFoundException e) { + throw new CommandException(String.format(MESSAGE_STUDENT_NOT_FOUND_IN_LESSON, studentName)); + } + } + + model.setLesson(targetLesson, newLesson); + + logger.fine("Successfully marked participation of students in lesson " + targetLesson.toString()); + String names = String.join(", ", studentNames.stream().map(x -> x.fullName).toList()); + return new CommandResult( + String.format(MESSAGE_SUCCESS, names, participationScore), + COMMAND_TYPE); + } + + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof MarkLessonParticipationCommand)) { + return false; + } + + MarkLessonParticipationCommand otherCommand = (MarkLessonParticipationCommand) other; + return index.equals(otherCommand.index) + && studentNames.equals(otherCommand.studentNames) + && (participationScore == otherCommand.participationScore); + } +} diff --git a/src/main/java/seedu/address/logic/commands/lesson/RemoveFromLessonCommand.java b/src/main/java/seedu/address/logic/commands/lesson/RemoveFromLessonCommand.java new file mode 100644 index 00000000000..6f5110d733d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/lesson/RemoveFromLessonCommand.java @@ -0,0 +1,93 @@ +package seedu.address.logic.commands.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.student.Name; +import seedu.address.model.student.Student; + +/** + * Removes multiple students from a specific lesson identified by its index. + */ +public class RemoveFromLessonCommand extends Command { + + public static final String COMMAND_WORD = "removefromlesson"; + public static final CommandType COMMAND_TYPE = CommandType.LESSON; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Removes students from the lesson identified by the index." + + "\nParameters: LESSON_INDEX " + + PREFIX_NAME + "NAME [" + PREFIX_NAME + "NAME]…" + + "\nExample: " + COMMAND_WORD + " 1 n/Alex Yeoh n/Harry Ng"; + + public static final String MESSAGE_REMOVE_FROM_LESSON_SUCCESS = "Removed students from Lesson: Date: %s; Time: %s"; + public static final String MESSAGE_STUDENT_NOT_FOUND = "Student(s) not found in the lesson."; + + private final Index lessonIndex; + private final List studentNames; + + /** + * @param lessonIndex The index of the lesson in the filtered lesson list. + * @param studentNames List of student names to remove from the lesson. + */ + public RemoveFromLessonCommand(Index lessonIndex, List studentNames) { + requireNonNull(lessonIndex); + requireNonNull(studentNames); + + this.lessonIndex = lessonIndex; + this.studentNames = studentNames; + } + + @Override + public CommandType getCommandType() { + return COMMAND_TYPE; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredLessonList(); + + if (lessonIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException("The lesson index provided is invalid."); + } + + Lesson targetLesson = lastShownList.get(lessonIndex.getZeroBased()); + Lesson editedLesson = new Lesson(targetLesson); + + for (Name studentName : studentNames) { + Student studentToRemove = model.findStudentByName(studentName) + .orElseThrow(() -> new CommandException("Student not found: " + studentName)); + + if (!editedLesson.hasStudent(studentToRemove)) { + throw new CommandException(MESSAGE_STUDENT_NOT_FOUND); + } + + editedLesson.removeStudent(studentToRemove); + } + + model.setLesson(targetLesson, editedLesson); + + String successMessage = String.format(MESSAGE_REMOVE_FROM_LESSON_SUCCESS, + editedLesson.getDate().getValue(), editedLesson.getTime().getValue()); + + return new CommandResult(successMessage, COMMAND_TYPE); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof RemoveFromLessonCommand // instanceof handles nulls + && lessonIndex.equals(((RemoveFromLessonCommand) other).lessonIndex) + && studentNames.equals(((RemoveFromLessonCommand) other).studentNames)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..f723da323fd 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -1,23 +1,25 @@ 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_COURSE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; 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; +import seedu.address.model.course.Course; +import seedu.address.model.student.Email; +import seedu.address.model.student.Name; +import seedu.address.model.student.Phone; +import seedu.address.model.student.Student; /** * Parses input arguments and creates a new AddCommand object @@ -30,32 +32,58 @@ public class AddCommandParser implements Parser { * @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); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, + PREFIX_EMAIL, PREFIX_COURSE); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, 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); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL); 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); + // Parse courses; if none are provided, use an empty set + Set courseList = parseCourses(argMultimap.getAllValues(PREFIX_COURSE)).orElse(new HashSet<>()); - return new AddCommand(person); + Student student = new Student(name, phone, email, courseList); + + return new AddCommand(student); + } + + /** + * Parses {@code Collection courses} into a {@code Set} if {@code courses} is non-empty. + * This method supports both CSV input and multiple `c/` prefixes. + */ + private Optional> parseCourses(Collection courses) throws ParseException { + assert courses != null; + + if (courses.isEmpty()) { + return Optional.empty(); // If no courses are provided, return empty Optional + } + + Set parsedCourses = new HashSet<>(); + + // For each course string (this could be a CSV or a single course) + for (String courseString : courses) { + + List splitCourses = ArgumentTokenizer.tokenizeWithDefault(courseString); + // Add each course after trimming whitespace + for (String course : splitCourses) { + parsedCourses.add(course.trim()); + } + } + + return Optional.of(ParserUtil.parseCourses(parsedCourses)); } /** * 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()); + private static boolean arePrefixesPresent(ArgumentMultimap argMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argMultimap.getValue(prefix).isPresent()); } - } diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..9c9d9efbe2d 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -14,10 +14,38 @@ import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.ExportCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ImportCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.consultation.AddConsultCommand; +import seedu.address.logic.commands.consultation.AddToConsultCommand; +import seedu.address.logic.commands.consultation.DeleteConsultCommand; +import seedu.address.logic.commands.consultation.ExportConsultCommand; +import seedu.address.logic.commands.consultation.ImportConsultCommand; +import seedu.address.logic.commands.consultation.ListConsultsCommand; +import seedu.address.logic.commands.consultation.RemoveFromConsultCommand; +import seedu.address.logic.commands.lesson.AddLessonCommand; +import seedu.address.logic.commands.lesson.AddToLessonCommand; +import seedu.address.logic.commands.lesson.DeleteLessonCommand; +import seedu.address.logic.commands.lesson.ListLessonsCommand; +import seedu.address.logic.commands.lesson.MarkLessonAttendanceCommand; +import seedu.address.logic.commands.lesson.MarkLessonParticipationCommand; +import seedu.address.logic.commands.lesson.RemoveFromLessonCommand; +import seedu.address.logic.parser.consultation.AddConsultCommandParser; +import seedu.address.logic.parser.consultation.AddToConsultCommandParser; +import seedu.address.logic.parser.consultation.DeleteConsultCommandParser; +import seedu.address.logic.parser.consultation.ExportConsultCommandParser; +import seedu.address.logic.parser.consultation.ImportConsultCommandParser; +import seedu.address.logic.parser.consultation.RemoveFromConsultCommandParser; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.logic.parser.lesson.AddLessonCommandParser; +import seedu.address.logic.parser.lesson.AddToLessonCommandParser; +import seedu.address.logic.parser.lesson.DeleteLessonCommandParser; +import seedu.address.logic.parser.lesson.MarkLessonAttendanceCommandParser; +import seedu.address.logic.parser.lesson.MarkLessonParticipationCommandParser; +import seedu.address.logic.parser.lesson.RemoveFromLessonCommandParser; /** * Parses user input. @@ -46,7 +74,8 @@ public Command parseCommand(String userInput) throws ParseException { final String commandWord = matcher.group("commandWord"); final String arguments = matcher.group("arguments"); - // Note to developers: Change the log level in config.json to enable lower level (i.e., FINE, FINER and lower) + // Note to developers: Change the log level in config.json to enable lower level + // (i.e., FINE, FINER and lower) // log messages such as the one below. // Lower level log messages are used sparingly to minimize noise in the code. logger.fine("Command word: " + commandWord + "; Arguments: " + arguments); @@ -56,12 +85,24 @@ public Command parseCommand(String userInput) throws ParseException { case AddCommand.COMMAND_WORD: return new AddCommandParser().parse(arguments); + case AddConsultCommand.COMMAND_WORD: + return new AddConsultCommandParser().parse(arguments); + + case RemoveFromConsultCommand.COMMAND_WORD: + return new RemoveFromConsultCommandParser().parse(arguments); + + case AddToConsultCommand.COMMAND_WORD: + return new AddToConsultCommandParser().parse(arguments); + case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); case DeleteCommand.COMMAND_WORD: return new DeleteCommandParser().parse(arguments); + case DeleteConsultCommand.COMMAND_WORD: + return new DeleteConsultCommandParser().parse(arguments); + case ClearCommand.COMMAND_WORD: return new ClearCommand(); @@ -71,12 +112,48 @@ public Command parseCommand(String userInput) throws ParseException { case ListCommand.COMMAND_WORD: return new ListCommand(); + case ListConsultsCommand.COMMAND_WORD: + return new ListConsultsCommand(); + + case ExportConsultCommand.COMMAND_WORD: + return new ExportConsultCommandParser().parse(arguments); + + case ImportConsultCommand.COMMAND_WORD: + return new ImportConsultCommandParser().parse(arguments); + case ExitCommand.COMMAND_WORD: return new ExitCommand(); case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case ExportCommand.COMMAND_WORD: + return new ExportCommandParser().parse(arguments); + + case ImportCommand.COMMAND_WORD: + return new ImportCommandParser().parse(arguments); + + case AddLessonCommand.COMMAND_WORD: + return new AddLessonCommandParser().parse(arguments); + + case DeleteLessonCommand.COMMAND_WORD: + return new DeleteLessonCommandParser().parse(arguments); + + case ListLessonsCommand.COMMAND_WORD: + return new ListLessonsCommand(); + + case AddToLessonCommand.COMMAND_WORD: + return new AddToLessonCommandParser().parse(arguments); + + case RemoveFromLessonCommand.COMMAND_WORD: + return new RemoveFromLessonCommandParser().parse(arguments); + + case MarkLessonAttendanceCommand.COMMAND_WORD: + return new MarkLessonAttendanceCommandParser().parse(arguments); + + case MarkLessonParticipationCommand.COMMAND_WORD: + return new MarkLessonParticipationCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java index 21e26887a83..8b16a3312da 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java @@ -22,6 +22,8 @@ public class ArgumentMultimap { /** Prefixes mapped to their respective arguments**/ private final Map> argMultimap = new HashMap<>(); + + /** * Associates the specified argument value with {@code prefix} key in this map. * If the map previously contained a mapping for the key, the new value is appended to the list of existing values. @@ -75,4 +77,14 @@ public void verifyNoDuplicatePrefixesFor(Prefix... prefixes) throws ParseExcepti throw new ParseException(Messages.getErrorMessageForDuplicatePrefixes(duplicatedPrefixes)); } } + + /** + * Returns true if the {@code ArgumentMultimap} contains all the given prefixes. + * + * @param prefixes Prefixes to test for. + * @return True if all prefixes are present. + */ + public boolean arePrefixesPresent(Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> getValue(prefix).isPresent()); + } } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..bae5bbc8555 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java @@ -1,5 +1,7 @@ package seedu.address.logic.parser; +import static seedu.address.logic.parser.CliSyntax.DEFAULT_DELIMITER; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -28,6 +30,22 @@ public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) { return extractArguments(argsString, positions); } + /** + * Tokenizes an arguments string and returns an {@code ArgumentMultimap} object that maps the default delimter to + * its argument values. Only the default delimiter will be recognized in the arguments string. + * + * @param argsString Arguments string of the form: {@code preamble value value ...} + * @return List that splits the argument using the default delimiter + */ + public static List tokenizeWithDefault(String argsString) { + List positions = findAllPrefixPositions(argsString, DEFAULT_DELIMITER); + ArgumentMultimap map = extractArguments(argsString, positions); + List items = map.getAllValues(DEFAULT_DELIMITER); + items.add(0, map.getPreamble()); + return items; + + } + /** * Finds all zero-based prefix positions in the given arguments string. * @@ -51,7 +69,11 @@ private static List findPrefixPositions(String argsString, Prefi while (prefixPosition != -1) { PrefixPosition extendedPrefix = new PrefixPosition(prefix, prefixPosition); positions.add(extendedPrefix); - prefixPosition = findPrefixPosition(argsString, prefix.getPrefix(), prefixPosition); + if (prefix.equals(DEFAULT_DELIMITER)) { + prefixPosition = findPrefixPosition(argsString, prefix.getPrefix(), prefixPosition + 1); + } else { + prefixPosition = findPrefixPosition(argsString, prefix.getPrefix(), prefixPosition); + } } return positions; @@ -70,9 +92,13 @@ private static List findPrefixPositions(String argsString, Prefi * {@code fromIndex} = 0, this method returns 5. */ private static int findPrefixPosition(String argsString, String prefix, int fromIndex) { - int prefixIndex = argsString.indexOf(" " + prefix, fromIndex); - return prefixIndex == -1 ? -1 - : prefixIndex + 1; // +1 as offset for whitespace + if (!prefix.equals(DEFAULT_DELIMITER.getPrefix())) { + int prefixIndex = argsString.indexOf(" " + prefix, fromIndex); + return prefixIndex == -1 ? -1 + : prefixIndex + 1; // +1 as offset for whitespace + } else { + return argsString.indexOf(prefix, fromIndex); + } } /** diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..98b9899d64e 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -6,10 +6,15 @@ public class CliSyntax { /* Prefix definitions */ + public static final Prefix PREFIX_ATTENDANCE = new Prefix("a/"); + public static final Prefix PREFIX_COURSE = new Prefix("c/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_EMAIL = new Prefix("e/"); 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_POINTS = new Prefix("pt/"); + public static final Prefix PREFIX_TIME = new Prefix("t/"); + public static final Prefix PREFIX_INDEX = new Prefix("i/"); + public static final Prefix DEFAULT_DELIMITER = new Prefix(";"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 3527fe76a3e..d09b4f58a1c 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -1,7 +1,14 @@ package seedu.address.logic.parser; +import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.parser.exceptions.ParseException; @@ -17,13 +24,36 @@ public class DeleteCommandParser implements Parser { * @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); + requireNonNull(args); + + List indicesList = ArgumentTokenizer.tokenizeWithDefault(args); + if (indicesList.isEmpty() || indicesList.stream().anyMatch(String::isEmpty)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + + Set indices; + Optional> optionalIndices = parseIndicesForDelete(indicesList); + assert optionalIndices.isPresent() : "Optional set of indices should not be empty or return null"; + indices = optionalIndices.get(); + return new DeleteCommand(indices); + } + + /** + * Parses the given {@code String} of arguments and returns an optional set of Index objects. + * @param indices A collection of Strings representing indices + * @return An optional set which contains Index objects + * @throws ParseException if the user input does not conform the expected format + */ + private Optional> parseIndicesForDelete(Collection indices) throws ParseException { + assert indices != null; + + if (indices.isEmpty()) { + return Optional.empty(); } + Collection indicesSet = indices.size() == 1 && indices.contains("") ? Collections.emptySet() : indices; + return Optional.of(ParserUtil.parseIndices(indicesSet)); } + } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..5b4df191cbf 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -2,22 +2,23 @@ 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_COURSE; 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.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Optional; import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.EditCommand.EditStudentDescriptor; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; +import seedu.address.model.course.Course; /** * Parses input arguments and creates a new EditCommand object @@ -32,7 +33,8 @@ public class EditCommandParser implements Parser { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_COURSE); Index index; @@ -42,44 +44,51 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL); - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + EditStudentDescriptor editStudentDescriptor = new EditStudentDescriptor(); if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + editStudentDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); } if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); + editStudentDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); } if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + editStudentDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); - if (!editPersonDescriptor.isAnyFieldEdited()) { + parseCoursesForEdit(argMultimap.getAllValues(PREFIX_COURSE)).ifPresent(editStudentDescriptor::setCourses); + + if (!editStudentDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); } - return new EditCommand(index, editPersonDescriptor); + return new EditCommand(index, editStudentDescriptor); } + /** - * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. - * If {@code tags} contain only one element which is an empty string, it will be parsed into a - * {@code Set} containing zero tags. + * Parses {@code Collection courses} into a {@code Set} if {@code courses} is non-empty. + * If {@code courses} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero courses. */ - private Optional> parseTagsForEdit(Collection tags) throws ParseException { - assert tags != null; + private Optional> parseCoursesForEdit(Collection courses) throws ParseException { + assert courses != null; - if (tags.isEmpty()) { + if (courses.isEmpty()) { return Optional.empty(); } - Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); + + Collection parsedCourses = new HashSet<>(); + for (String course : courses) { + String[] parsedCourse = course.split(";"); + Arrays.stream(parsedCourse).forEach(parsedCourses::add); + } + Collection courseSet = courses.size() == 1 && courses.contains("") + ? Collections.emptySet() + : parsedCourses; + return Optional.of(ParserUtil.parseCourses(courseSet)); } } diff --git a/src/main/java/seedu/address/logic/parser/ExportCommandParser.java b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java new file mode 100644 index 00000000000..3a378dab794 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java @@ -0,0 +1,52 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.ExportCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ExportCommand object + */ +public class ExportCommandParser implements Parser { + public static final String MESSAGE_INVALID_FILENAME = + "Filename cannot contain periods or slashes. Please provide a simple filename."; + private static final String INVALID_FILENAME_CHARS = "[./\\\\]"; + + /** + * Parses the given {@code String} of arguments in the context of the ExportCommand + * and returns an ExportCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ExportCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportCommand.MESSAGE_USAGE)); + } + + boolean isForceExport = false; + String filename = trimmedArgs; + + // Check if force flag is present + if (trimmedArgs.equals(ExportCommand.FORCE_FLAG) + || trimmedArgs.startsWith(ExportCommand.FORCE_FLAG + " ")) { + isForceExport = true; + // Extract filename after the force flag and space + filename = trimmedArgs.equals(ExportCommand.FORCE_FLAG) ? "" + : trimmedArgs.substring(ExportCommand.FORCE_FLAG.length()).trim(); + } + + // Validate filename + if (filename.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportCommand.MESSAGE_USAGE)); + } + + if (filename.matches(".*" + INVALID_FILENAME_CHARS + ".*")) { + throw new ParseException(MESSAGE_INVALID_FILENAME); + } + + return new ExportCommand(filename, isForceExport); + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 2867bde857b..09c6d59755d 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -1,33 +1,69 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COURSE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.student.IsStudentOfCoursePredicate; +import seedu.address.model.student.NameContainsKeywordsPredicate; +import seedu.address.model.student.Student; /** * Parses input arguments and creates a new FindCommand object */ public class FindCommandParser implements Parser { + public static final Prefix[] PREFIXES = {PREFIX_NAME, PREFIX_COURSE}; + /** * 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()) { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIXES); + boolean allEmpty = true; + for (Prefix pre : PREFIXES) { + if (!argMultimap.getAllValues(pre).isEmpty()) { + allEmpty = false; + } + } + if (allEmpty) { throw new ParseException( String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); } - String[] nameKeywords = trimmedArgs.split("\\s+"); + List> predicateList = new ArrayList<>(); + for (Prefix pre : PREFIXES) { + combinePredicates(argMultimap, pre, predicateList); + } - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + return new FindCommand(predicateList); } + /** + * Creates a combined predicate for the given prefix from the ArgumentMultimap. + */ + private void combinePredicates(ArgumentMultimap argMultimap, + Prefix prefix, + List> predicateList) { + if (argMultimap.getValue(prefix).isPresent()) { + if (prefix.equals(PREFIX_NAME)) { + argMultimap.getAllValues(prefix) + .forEach(pre -> predicateList.add( + new NameContainsKeywordsPredicate(ArgumentTokenizer.tokenizeWithDefault(pre)))); + } else if (prefix.equals(PREFIX_COURSE)) { + argMultimap.getAllValues(prefix) + .forEach(pre -> predicateList.add( + new IsStudentOfCoursePredicate(ArgumentTokenizer.tokenizeWithDefault(pre)))); + } + } + } } + diff --git a/src/main/java/seedu/address/logic/parser/ImportCommandParser.java b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java new file mode 100644 index 00000000000..cb8408e4970 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java @@ -0,0 +1,34 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.ImportCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ImportCommand object + */ +public class ImportCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ImportCommand + * and returns an ImportCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ImportCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE) + ); + } + + // Validate that the path doesn't try to access project directory + if (trimmedArgs.contains("..") || trimmedArgs.startsWith("/") || trimmedArgs.startsWith("./")) { + throw new ParseException(ImportCommand.MESSAGE_FILE_OUTSIDE_PROJECT); + } + + return new ImportCommand(trimmedArgs); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..7e4a8fa139c 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -8,12 +8,14 @@ import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.commands.lesson.MarkLessonParticipationCommand; 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; +import seedu.address.model.course.Course; +import seedu.address.model.datetime.Date; +import seedu.address.model.datetime.Time; +import seedu.address.model.student.Email; +import seedu.address.model.student.Name; +import seedu.address.model.student.Phone; /** * Contains utility methods used for parsing strings in the various *Parser classes. @@ -21,6 +23,25 @@ public class ParserUtil { public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + public static final String MESSAGE_INVALID_ATTENDANCE = + "Invalid attendance entry. Please enter 1/Y/y for yes and 0/N/n for no."; + public static final String MESSAGE_INVALID_PARTICIPATION = + "Participation should be an integer between 0-100 inclusive."; + + /** + * Parses a {@code String date} into a {@code Date}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code date} is invalid. + */ + public static Date parseDate(String date) throws ParseException { + requireNonNull(date); + String trimmedDate = date.trim(); + if (!Date.isValidDate(trimmedDate)) { + throw new ParseException(Date.MESSAGE_CONSTRAINTS); + } + return new Date(trimmedDate); + } /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be @@ -35,6 +56,18 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } + /** + * Parses {@code Collection indices} into a {@code Set}. + */ + public static Set parseIndices(Collection indices) throws ParseException { + requireNonNull(indices); + final Set indicesSet = new HashSet<>(); + for (String index : indices) { + indicesSet.add(parseIndex(index)); + } + return indicesSet; + } + /** * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. @@ -65,21 +98,6 @@ public static Phone parsePhone(String phone) throws ParseException { return new Phone(trimmedPhone); } - /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code address} is invalid. - */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); - } - return new Address(trimmedAddress); - } - /** * Parses a {@code String email} into an {@code Email}. * Leading and trailing whitespaces will be trimmed. @@ -96,29 +114,82 @@ public static Email parseEmail(String email) throws ParseException { } /** - * Parses a {@code String tag} into a {@code Tag}. + * Parses a {@code String course} into a {@code Course}. * 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); + public static Course parseCourse(String course) throws ParseException { + requireNonNull(course); + String trimmedCourse = course.trim(); + if (!Course.isValidCourse(trimmedCourse)) { + throw new ParseException(Course.MESSAGE_CONSTRAINTS); + } + return new Course(trimmedCourse); + } + + /** + * Parses {@code Collection courses} into a {@code Set}. + */ + public static Set parseCourses(Collection courses) throws ParseException { + requireNonNull(courses); + final Set courseSet = new HashSet<>(); + for (String courseName : courses) { + courseSet.add(parseCourse(courseName)); + } + return courseSet; + } + + /** + * Parses a {@code String time} into a {@code Time}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code time} is invalid. + */ + public static Time parseTime(String time) throws ParseException { + requireNonNull(time); + String trimmedTime = time.trim(); + if (!Time.isValidTime(trimmedTime)) { + throw new ParseException(Time.MESSAGE_CONSTRAINTS); } - return new Tag(trimmedTag); + return new Time(trimmedTime); + } + + /** + * Parses a {@code attendance} into its respective boolean. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given attendance string is invalid. + */ + public static boolean parseAttendance(String attendance) throws ParseException { + requireNonNull(attendance); + String trimmedAttendance = attendance.trim(); + return switch (trimmedAttendance) { + case "1", "Y", "y" -> true; + case "0", "N", "n" -> false; + default -> + throw new ParseException(MESSAGE_INVALID_ATTENDANCE); + }; } /** - * Parses {@code Collection tags} into a {@code Set}. + * Parses a {@code points} into an integer. Points should be able to fit within a Java int primitive type. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given point value is invalid. */ - public static Set parseTags(Collection tags) throws ParseException { - requireNonNull(tags); - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); + public static int parsePoints(String points) throws ParseException { + requireNonNull(points); + String trimmedPoints = points.trim(); + int participationPoints = -1; // default invalid number + try { + participationPoints = Integer.parseInt(trimmedPoints); + } catch (NumberFormatException e) { + throw new ParseException(MESSAGE_INVALID_PARTICIPATION); + } + if (!MarkLessonParticipationCommand.isValidParticipation(participationPoints)) { + throw new ParseException(MESSAGE_INVALID_PARTICIPATION); } - return tagSet; + return participationPoints; } } diff --git a/src/main/java/seedu/address/logic/parser/consultation/AddConsultCommandParser.java b/src/main/java/seedu/address/logic/parser/consultation/AddConsultCommandParser.java new file mode 100644 index 00000000000..65b3335f71e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/consultation/AddConsultCommandParser.java @@ -0,0 +1,46 @@ +package seedu.address.logic.parser.consultation; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TIME; + +import java.util.List; + +import seedu.address.logic.commands.consultation.AddConsultCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.consultation.Consultation; +import seedu.address.model.datetime.Date; +import seedu.address.model.datetime.Time; + +/** + * Parses input arguments and creates a new AddCommand object + */ +public class AddConsultCommandParser 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 AddConsultCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_DATE, PREFIX_TIME); + + if (!argMultimap.arePrefixesPresent(PREFIX_DATE, PREFIX_TIME) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddConsultCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_DATE, PREFIX_TIME); + Date date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + Time time = ParserUtil.parseTime(argMultimap.getValue(PREFIX_TIME).get()); + + Consultation consult = new Consultation(date, time, List.of()); + + return new AddConsultCommand(consult); + } +} diff --git a/src/main/java/seedu/address/logic/parser/consultation/AddToConsultCommandParser.java b/src/main/java/seedu/address/logic/parser/consultation/AddToConsultCommandParser.java new file mode 100644 index 00000000000..8ef833a1b2a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/consultation/AddToConsultCommandParser.java @@ -0,0 +1,71 @@ +package seedu.address.logic.parser.consultation; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.consultation.AddToConsultCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.student.Name; + +/** + * Parses input arguments and creates a new AddToConsultCommand object. + */ +public class AddToConsultCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddToConsultCommand + * and returns an AddToConsultCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + public AddToConsultCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_INDEX); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddToConsultCommand.MESSAGE_USAGE), pe); + } + + if (argMultimap.getAllValues(PREFIX_NAME).isEmpty() && argMultimap.getAllValues(PREFIX_INDEX).isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddToConsultCommand.MESSAGE_USAGE)); + } + + // Use a regular for-loop to handle exceptions + List studentNames = new ArrayList<>(); + for (String nameString : argMultimap.getAllValues(PREFIX_NAME)) { + Name name; + try { + name = ParserUtil.parseName(nameString); + } catch (ParseException e) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS, e); // Handle ParseException directly + } + studentNames.add(name); + } + + + List indices = new ArrayList<>(); + for (String stringNameIndex: argMultimap.getAllValues(PREFIX_INDEX)) { + // throws ParseException, with the message being invalid index + Index nameIndex = ParserUtil.parseIndex(stringNameIndex); + indices.add(nameIndex); + } + + return new AddToConsultCommand(index, studentNames, indices); + } +} diff --git a/src/main/java/seedu/address/logic/parser/consultation/DeleteConsultCommandParser.java b/src/main/java/seedu/address/logic/parser/consultation/DeleteConsultCommandParser.java new file mode 100644 index 00000000000..5420e785c87 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/consultation/DeleteConsultCommandParser.java @@ -0,0 +1,69 @@ +package seedu.address.logic.parser.consultation; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.consultation.DeleteConsultCommand; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + + +/** + * Parses input arguments and creates a new DeleteConsultCommand object + */ +public class DeleteConsultCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteConsultCommand + * and returns a DeleteConsultCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + public DeleteConsultCommand parse(String args) throws ParseException { + requireNonNull(args); + + List indicesList = ArgumentTokenizer.tokenizeWithDefault(args); + if (indicesList.isEmpty() || indicesList.stream().anyMatch(String::isEmpty)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteConsultCommand.MESSAGE_USAGE)); + } + + + try { + Set indices; + Optional> optionalIndices = parseIndicesForDelete(indicesList); + assert optionalIndices.isPresent() : "Optional set of indices should not be empty or return null"; + indices = optionalIndices.get(); + return new DeleteConsultCommand(indices); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteConsultCommand.MESSAGE_USAGE), pe); + } + } + + /** + * Parses the given {@code String} of arguments and returns an optional set of Index objects. + * @param indices A collection of Strings representing indices + * @return An optional set which contains Index objects + * @throws ParseException if the user input does not conform the expected format + */ + private Optional> parseIndicesForDelete(Collection indices) throws ParseException { + assert indices != null; + + if (indices.isEmpty()) { + return Optional.empty(); + } + Collection indicesSet = indices.size() == 1 && indices.contains("") ? Collections.emptySet() : indices; + return Optional.of(ParserUtil.parseIndices(indicesSet)); + } + + +} diff --git a/src/main/java/seedu/address/logic/parser/consultation/ExportConsultCommandParser.java b/src/main/java/seedu/address/logic/parser/consultation/ExportConsultCommandParser.java new file mode 100644 index 00000000000..b660d47eedd --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/consultation/ExportConsultCommandParser.java @@ -0,0 +1,55 @@ +package seedu.address.logic.parser.consultation; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.consultation.ExportConsultCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ExportConsultCommand object + */ +public class ExportConsultCommandParser implements Parser { + + public static final String MESSAGE_INVALID_FILENAME = + "Filename cannot contain periods or slashes. Please provide a simple filename."; + private static final String INVALID_FILENAME_CHARS = "[./\\\\]"; + + /** + * Parses the given {@code String} of arguments in the context of the ExportConsultCommand + * and returns an ExportConsultCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public ExportConsultCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportConsultCommand.MESSAGE_USAGE)); + } + + boolean isForceExport = false; + String filename = trimmedArgs; + + // Check if force flag is present + if (trimmedArgs.equals(ExportConsultCommand.FORCE_FLAG) + || trimmedArgs.startsWith(ExportConsultCommand.FORCE_FLAG + " ")) { + isForceExport = true; + // Extract filename after the force flag and space + filename = trimmedArgs.equals(ExportConsultCommand.FORCE_FLAG) ? "" + : trimmedArgs.substring(ExportConsultCommand.FORCE_FLAG.length()).trim(); + } + + // Validate filename + if (filename.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportConsultCommand.MESSAGE_USAGE)); + } + + if (filename.matches(".*" + INVALID_FILENAME_CHARS + ".*")) { + throw new ParseException(MESSAGE_INVALID_FILENAME); + } + + return new ExportConsultCommand(filename, isForceExport); + } +} diff --git a/src/main/java/seedu/address/logic/parser/consultation/ImportConsultCommandParser.java b/src/main/java/seedu/address/logic/parser/consultation/ImportConsultCommandParser.java new file mode 100644 index 00000000000..47e9c05d772 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/consultation/ImportConsultCommandParser.java @@ -0,0 +1,36 @@ +package seedu.address.logic.parser.consultation; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.consultation.ImportConsultCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ImportConsultCommand object + */ +public class ImportConsultCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ImportConsultCommand + * and returns an ImportConsultCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public ImportConsultCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportConsultCommand.MESSAGE_USAGE) + ); + } + + // Validate that the path doesn't try to access project directory + if (trimmedArgs.contains("..") || trimmedArgs.startsWith("/") || trimmedArgs.startsWith("./")) { + throw new ParseException(ImportConsultCommand.MESSAGE_FILE_OUTSIDE_PROJECT); + } + + return new ImportConsultCommand(trimmedArgs); + } +} diff --git a/src/main/java/seedu/address/logic/parser/consultation/RemoveFromConsultCommandParser.java b/src/main/java/seedu/address/logic/parser/consultation/RemoveFromConsultCommandParser.java new file mode 100644 index 00000000000..39575dcb464 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/consultation/RemoveFromConsultCommandParser.java @@ -0,0 +1,64 @@ +package seedu.address.logic.parser.consultation; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.consultation.RemoveFromConsultCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.student.Name; + +/** + * Parses input arguments and creates a new RemoveFromConsultCommand object + */ +public class RemoveFromConsultCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the RemoveFromConsultCommand + * and returns a RemoveFromConsultCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + @Override + public RemoveFromConsultCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME); + + Index index; + + try { + // Parse index + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + RemoveFromConsultCommand.MESSAGE_USAGE), pe); + } + + if (argMultimap.getAllValues(PREFIX_NAME).isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + RemoveFromConsultCommand.MESSAGE_USAGE)); + } + + // Use a regular for-loop to parse names and handle exceptions + List studentNames = new ArrayList<>(); + for (String nameString : argMultimap.getAllValues(PREFIX_NAME)) { + try { + Name name = ParserUtil.parseName(nameString); + studentNames.add(name); + } catch (ParseException e) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS, e); // Handle ParseException directly + } + } + + return new RemoveFromConsultCommand(index, studentNames); + } +} diff --git a/src/main/java/seedu/address/logic/parser/lesson/AddLessonCommandParser.java b/src/main/java/seedu/address/logic/parser/lesson/AddLessonCommandParser.java new file mode 100644 index 00000000000..291deb28d9a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/lesson/AddLessonCommandParser.java @@ -0,0 +1,44 @@ +package seedu.address.logic.parser.lesson; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TIME; + +import seedu.address.logic.commands.lesson.AddLessonCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.datetime.Date; +import seedu.address.model.datetime.Time; +import seedu.address.model.lesson.Lesson; + +/** + * Parses input arguments and creates a new AddLessonCommand object + */ +public class AddLessonCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddLessonCommand + * and returns an AddLessonCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddLessonCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_DATE, PREFIX_TIME); + + if (!argMultimap.arePrefixesPresent(PREFIX_DATE, PREFIX_TIME) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddLessonCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_DATE, PREFIX_TIME); + Date date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + Time time = ParserUtil.parseTime(argMultimap.getValue(PREFIX_TIME).get()); + + Lesson lesson = new Lesson(date, time); + + return new AddLessonCommand(lesson); + } +} diff --git a/src/main/java/seedu/address/logic/parser/lesson/AddToLessonCommandParser.java b/src/main/java/seedu/address/logic/parser/lesson/AddToLessonCommandParser.java new file mode 100644 index 00000000000..c62e87c6a13 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/lesson/AddToLessonCommandParser.java @@ -0,0 +1,77 @@ +package seedu.address.logic.parser.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.lesson.AddToLessonCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.student.Name; + +/** + * Parses input arguments and creates a new AddToLessonCommand object. + */ +public class AddToLessonCommandParser implements Parser { + + private final Logger logger = LogsCenter.getLogger(AddToLessonCommandParser.class); + + /** + * Parses the given {@code String} of arguments in the context of the AddToLessonCommand + * and returns an AddToLessonCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + public AddToLessonCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_INDEX); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + logger.warning("Index was not provided"); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddToLessonCommand.MESSAGE_USAGE), pe); + } + + if (argMultimap.getAllValues(PREFIX_NAME).isEmpty() && argMultimap.getAllValues(PREFIX_INDEX).isEmpty()) { + logger.warning("No names and no student indices were provided"); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddToLessonCommand.MESSAGE_USAGE)); + } + + // Parse names + List studentNames = new ArrayList<>(); + for (String nameString : argMultimap.getAllValues(PREFIX_NAME)) { + try { + Name name = ParserUtil.parseName(nameString); + studentNames.add(name); + } catch (ParseException e) { + logger.warning("Name " + nameString + " could not be parsed properly"); + throw new ParseException(Name.MESSAGE_CONSTRAINTS, e); + } + } + + List indices = new ArrayList<>(); + for (String stringIndex : argMultimap.getAllValues(PREFIX_INDEX)) { + // throws ParseException, with the message being invalid index + Index studentIndex = ParserUtil.parseIndex(stringIndex); + indices.add(studentIndex); + } + + return new AddToLessonCommand(index, studentNames, indices); + } +} diff --git a/src/main/java/seedu/address/logic/parser/lesson/DeleteLessonCommandParser.java b/src/main/java/seedu/address/logic/parser/lesson/DeleteLessonCommandParser.java new file mode 100644 index 00000000000..7e74a452789 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/lesson/DeleteLessonCommandParser.java @@ -0,0 +1,65 @@ +package seedu.address.logic.parser.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.lesson.DeleteLessonCommand; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteLessonCommand object + */ +public class DeleteLessonCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteLessonCommand + * and returns a DeleteLessonCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + public DeleteLessonCommand parse(String args) throws ParseException { + requireNonNull(args); + + List indicesList = ArgumentTokenizer.tokenizeWithDefault(args); + if (indicesList.isEmpty() || indicesList.stream().anyMatch(String::isEmpty)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteLessonCommand.MESSAGE_USAGE)); + } + + try { + Set indices; + Optional> optionalIndices = parseIndicesForDelete(indicesList); + assert optionalIndices.isPresent() : "Optional set of indices should not be empty or return null"; + indices = optionalIndices.get(); + return new DeleteLessonCommand(indices); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteLessonCommand.MESSAGE_USAGE), pe); + } + } + + /** + * Parses the given {@code String} of arguments and returns an optional set of Index objects. + * @param indices A collection of Strings representing indices + * @return An optional set which contains Index objects + * @throws ParseException if the user input does not conform the expected format + */ + private Optional> parseIndicesForDelete(Collection indices) throws ParseException { + assert indices != null; + + if (indices.isEmpty()) { + return Optional.empty(); + } + Collection indicesSet = indices.size() == 1 && indices.contains("") ? Collections.emptySet() : indices; + return Optional.of(ParserUtil.parseIndices(indicesSet)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/lesson/MarkLessonAttendanceCommandParser.java b/src/main/java/seedu/address/logic/parser/lesson/MarkLessonAttendanceCommandParser.java new file mode 100644 index 00000000000..25a919bee5a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/lesson/MarkLessonAttendanceCommandParser.java @@ -0,0 +1,71 @@ +package seedu.address.logic.parser.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ATTENDANCE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.lesson.MarkLessonAttendanceCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.student.Name; + +/** + * Parses input arguments and creates a new MarkLessonAttendanceCommand object. + */ +public class MarkLessonAttendanceCommandParser implements Parser { + + public static final String MESSAGE_TOO_MANY_ATTENDANCE_ARGUMENTS = + "Number of attendance arguments must be exactly 1!"; + private final Logger logger = LogsCenter.getLogger(MarkLessonAttendanceCommandParser.class); + + /** + * Parses the given {@code String} of arguments in the context of the MarkLessonAttendanceCommand + * and returns an MarkLessonAttendanceCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + public MarkLessonAttendanceCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_ATTENDANCE); + + if (argMultimap.getPreamble().isEmpty() || !argMultimap.arePrefixesPresent(PREFIX_NAME, PREFIX_ATTENDANCE)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + MarkLessonAttendanceCommand.MESSAGE_USAGE)); + } + + // too many attendance arguments + if (argMultimap.getAllValues(PREFIX_ATTENDANCE).size() > 1) { + throw new ParseException(MESSAGE_TOO_MANY_ATTENDANCE_ARGUMENTS); + } + + // Parse index + Index index = ParserUtil.parseIndex(argMultimap.getPreamble()); + + // Parse names + List studentNames = new ArrayList<>(); + for (String nameString : argMultimap.getAllValues(PREFIX_NAME)) { + try { + Name name = ParserUtil.parseName(nameString); + studentNames.add(name); + } catch (ParseException e) { + logger.warning("Name " + nameString + " could not be parsed properly"); + throw new ParseException(Name.MESSAGE_CONSTRAINTS, e); + } + } + // Parse attendance + boolean attendance = ParserUtil.parseAttendance(argMultimap.getValue(PREFIX_ATTENDANCE).get()); + + return new MarkLessonAttendanceCommand(index, studentNames, attendance); + } +} diff --git a/src/main/java/seedu/address/logic/parser/lesson/MarkLessonParticipationCommandParser.java b/src/main/java/seedu/address/logic/parser/lesson/MarkLessonParticipationCommandParser.java new file mode 100644 index 00000000000..1a6d593e0dc --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/lesson/MarkLessonParticipationCommandParser.java @@ -0,0 +1,71 @@ +package seedu.address.logic.parser.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POINTS; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.lesson.MarkLessonParticipationCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.student.Name; + +/** + * Parses input arguments and creates a new MarkLessonAttendanceCommand object. + */ +public class MarkLessonParticipationCommandParser implements Parser { + + public static final String MESSAGE_TOO_MANY_PARTICIPATION_ARGUMENTS = + "Number of participation arguments must be exactly 1!"; + private final Logger logger = LogsCenter.getLogger(MarkLessonParticipationCommandParser.class); + + /** + * Parses the given {@code String} of arguments in the context of the MarkLessonAttendanceCommand + * and returns an MarkLessonAttendanceCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + public MarkLessonParticipationCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_POINTS); + + if (argMultimap.getPreamble().isEmpty() || !argMultimap.arePrefixesPresent(PREFIX_NAME, PREFIX_POINTS)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + MarkLessonParticipationCommand.MESSAGE_USAGE)); + } + + // too many attendance arguments + if (argMultimap.getAllValues(PREFIX_POINTS).size() > 1) { + throw new ParseException(MESSAGE_TOO_MANY_PARTICIPATION_ARGUMENTS); + } + + // Parse index + Index index = ParserUtil.parseIndex(argMultimap.getPreamble()); + + // Parse names + List studentNames = new ArrayList<>(); + for (String nameString : argMultimap.getAllValues(PREFIX_NAME)) { + try { + Name name = ParserUtil.parseName(nameString); + studentNames.add(name); + } catch (ParseException e) { + logger.warning("Name " + nameString + " could not be parsed properly"); + throw new ParseException(Name.MESSAGE_CONSTRAINTS, e); + } + } + // Parse participation + int participationScore = ParserUtil.parsePoints(argMultimap.getValue(PREFIX_POINTS).get()); + + return new MarkLessonParticipationCommand(index, studentNames, participationScore); + } +} diff --git a/src/main/java/seedu/address/logic/parser/lesson/RemoveFromLessonCommandParser.java b/src/main/java/seedu/address/logic/parser/lesson/RemoveFromLessonCommandParser.java new file mode 100644 index 00000000000..4be7354bc35 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/lesson/RemoveFromLessonCommandParser.java @@ -0,0 +1,64 @@ +package seedu.address.logic.parser.lesson; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.lesson.RemoveFromLessonCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.student.Name; + +/** + * Parses input arguments and creates a new RemoveFromLessonCommand object. + */ +public class RemoveFromLessonCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the + * RemoveFromLessonCommand + * and returns a RemoveFromLessonCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected + * format + */ + @Override + public RemoveFromLessonCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + RemoveFromLessonCommand.MESSAGE_USAGE), pe); + } + + if (argMultimap.getAllValues(PREFIX_NAME).isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + RemoveFromLessonCommand.MESSAGE_USAGE)); + } + + // Parse names + List studentNames = new ArrayList<>(); + for (String nameString : argMultimap.getAllValues(PREFIX_NAME)) { + try { + Name name = ParserUtil.parseName(nameString); + studentNames.add(name); + } catch (ParseException e) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS, e); + } + } + + return new RemoveFromLessonCommand(index, studentNames); + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..4d0702f98c6 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -1,37 +1,52 @@ package seedu.address.model; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.util.List; +import java.util.Objects; import javafx.collections.ObservableList; -import seedu.address.commons.util.ToStringBuilder; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; +import seedu.address.model.consultation.Consultation; +import seedu.address.model.consultation.UniqueConsultList; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.lesson.UniqueLessonList; +import seedu.address.model.lesson.exceptions.LessonNotFoundException; +import seedu.address.model.student.Student; +import seedu.address.model.student.UniqueStudentList; /** * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) + * Duplicate students are not allowed (by .isSameStudent comparison) */ public class AddressBook implements ReadOnlyAddressBook { - private final UniquePersonList persons; + private final UniqueStudentList students; + private final UniqueConsultList consults; // Use UniqueConsultList instead of ObservableList + private final UniqueLessonList lessons; /* - * 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 + * 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. + * Note that non-static init blocks are not recommended to use. There are other + * ways to avoid duplication + * among constructors. */ { - persons = new UniquePersonList(); + students = new UniqueStudentList(); + consults = new UniqueConsultList(); // Initialize UniqueConsultList + lessons = new UniqueLessonList(); } - public AddressBook() {} + + public AddressBook() { + } /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} + * Creates an AddressBook using the data in the {@code toBeCopied} */ public AddressBook(ReadOnlyAddressBook toBeCopied) { this(); @@ -41,11 +56,71 @@ public AddressBook(ReadOnlyAddressBook toBeCopied) { //// list overwrite operations /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. + * Replaces the contents of the student list with {@code students}. + * {@code students} must not contain duplicate students. + */ + public void setStudents(List students) { + this.students.setStudents(students); + } + + // Consultation-level operations + /** + * Returns true if a consultation with the same details as {@code consult} exists in TAHub. + */ + public boolean hasConsult(Consultation consult) { + requireNonNull(consult); + return consults.contains(consult); // Use UniqueConsultList's contains method + } + + /** + * Adds a consultation to the address book. + * The consultation must not already exist in the address book. + */ + public void addConsult(Consultation consult) { + requireNonNull(consult); + consults.add(consult); + consults.sort(); + } + + /** + * Replaces the contents of the consultation list with {@code consults}. + */ + public void setConsults(List consults) { + this.consults.setConsults(consults); // Use setConsults method from UniqueConsultList + this.consults.sort(); + } + + /** + * Replaces the given consultation {@code target} in the list with {@code editedConsult}. + * {@code target} must exist in TAHub. + */ + public void setConsult(Consultation target, Consultation editedConsult) { + requireAllNonNull(target, editedConsult); + consults.setConsult(target, editedConsult); // Use setConsult method from UniqueConsultList + } + + /** + * Removes {@code consult} from this {@code AddressBook}. + * {@code consult} must exist in TAHub. + */ + public void removeConsult(Consultation consult) { + requireNonNull(consult); + consults.remove(consult); // Use remove method from UniqueConsultList + } + + /** + * Returns an unmodifiable view of the consultation list. */ - public void setPersons(List persons) { - this.persons.setPersons(persons); + public ObservableList getConsultList() { + return consults.asUnmodifiableObservableList(); // Use UniqueConsultList's unmodifiable list view + } + + /** + * Replaces the contents of the lesson list with {@code lesson}. + */ + public void setLessons(List lessons) { + this.lessons.setLessons(lessons); + this.lessons.sort(); } /** @@ -53,61 +128,106 @@ public void setPersons(List persons) { */ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); - - setPersons(newData.getPersonList()); + setStudents(newData.getStudentList()); + setConsults(newData.getConsultList()); + setLessons(newData.getLessonList()); } - //// person-level operations + //// student-level operations /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a student with the same identity as {@code student} exists in + * the address book. */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); + public boolean hasStudent(Student student) { + requireNonNull(student); + return students.contains(student); } /** - * Adds a person to the address book. - * The person must not already exist in the address book. + * Adds a student to the address book. + * The student must not already exist in the address book. */ - public void addPerson(Person p) { - persons.add(p); + public void addStudent(Student p) { + students.add(p); } /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. + * Replaces the given student {@code target} in the list with + * {@code editedStudent}. * {@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. + * The student identity of {@code editedStudent} must not be the same + * as another existing student in the address book. */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); + public void setStudent(Student target, Student editedStudent) { + requireNonNull(editedStudent); + + // Set Student in Student List + students.setStudent(target, editedStudent); - persons.setPerson(target, editedPerson); + // Set Student in Consultation List + List consultsWithEditedStudent = consults.filtered(c -> c.hasStudent(target)); + consultsWithEditedStudent.forEach(c -> { + Consultation newConsult = new Consultation(c); + newConsult.setStudent(target, editedStudent); + setConsult(c, newConsult); + }); + + // Set Student in Lesson List + List lessonsWithEditedStudent = lessons.filtered(l -> l.hasStudent(target)).stream().toList(); + lessonsWithEditedStudent.forEach(l -> { + Lesson newLesson = new Lesson(l); + newLesson.setStudent(target, editedStudent); + setLesson(l, newLesson); + }); } /** * Removes {@code key} from this {@code AddressBook}. + * Also removes the student from all consultations and lessons. * {@code key} must exist in the address book. */ - public void removePerson(Person key) { - persons.remove(key); + public void removeStudent(Student key) { + students.remove(key); + + // Remove from consultations + List consultsWithDeletedStudent = consults.filtered(c -> c.hasStudent(key)); + consultsWithDeletedStudent.forEach(c -> { + Consultation newConsult = new Consultation(c); + newConsult.removeStudent(key); + setConsult(c, newConsult); + }); + + // Remove from lessons + List lessonsWithDeletedStudent = lessons.filtered(l -> l.hasStudent(key)).stream().toList(); + lessonsWithDeletedStudent.forEach(l -> { + Lesson newLesson = new Lesson(l); + newLesson.removeStudent(key); + setLesson(l, newLesson); + }); } //// util methods @Override public String toString() { - return new ToStringBuilder(this) - .add("persons", persons) - .toString(); + return AddressBook.class.getCanonicalName() + "{students=" + students.asUnmodifiableObservableList() + + ", consults=" + consults.asUnmodifiableObservableList() + + ", lessons=" + lessons.asUnmodifiableObservableList() + "}"; } @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); + public ObservableList getStudentList() { + return students.asUnmodifiableObservableList(); } + /** + * Checks if this {@code AddressBook} is equal to another object. + * + * @param other The object to compare with. + * @return true if both AddressBooks have the same students, consultations, and + * lessons, false otherwise. + */ @Override public boolean equals(Object other) { if (other == this) { @@ -120,11 +240,80 @@ public boolean equals(Object other) { } AddressBook otherAddressBook = (AddressBook) other; - return persons.equals(otherAddressBook.persons); + return students.equals(otherAddressBook.students) + && consults.equals(otherAddressBook.consults) + && lessons.equals(otherAddressBook.lessons); } + /** + * Returns the hash code for this {@code AddressBook}. + * + * @return The hash code based on students, consultations, and lessons. + */ @Override public int hashCode() { - return persons.hashCode(); + return Objects.hash(students, consults, lessons); + } + + /** + * Checks if the address book contains the specified {@code Lesson}. + * + * @param lesson The lesson to check. + * @return true if the lesson exists in the address book, false otherwise. + * @throws NullPointerException if {@code lesson} is null. + */ + public boolean hasLesson(Lesson lesson) { + requireNonNull(lesson); + return lessons.contains(lesson); + } + + /** + * Adds a {@code Lesson} to the address book and sorts the lesson list by date. + * If two lessons have the same date, they are further sorted by time. + * + * @param lesson The lesson to add. + * @throws NullPointerException if {@code lesson} is null. + */ + public void addLesson(Lesson lesson) { + requireNonNull(lesson); + lessons.add(lesson); + lessons.sort(); + } + + /** + * Removes a {@code Lesson} from the address book. + * + * @param lesson The lesson to remove. + * @throws NullPointerException if {@code lesson} is null. + */ + public void removeLesson(Lesson lesson) { + lessons.remove(lesson); + } + + /** + * Returns an unmodifiable view of the lesson list. + * + * @return An unmodifiable {@code ObservableList} containing all lessons in the + * address book. + */ + public ObservableList getLessonList() { + return lessons.asUnmodifiableObservableList(); + } + + /** + * Replaces the given lesson {@code target} in the list with + * {@code editedLesson}. + * {@code target} must exist in the address book. + * + * @param target The lesson to be replaced. + * @param editedLesson The new lesson to replace the target. + * @throws NullPointerException if {@code target} or {@code editedLesson} is + * null. + * @throws LessonNotFoundException if {@code target} could not be found in the + * list. + */ + public void setLesson(Lesson target, Lesson editedLesson) { + requireAllNonNull(target, editedLesson); + lessons.setLesson(target, editedLesson); } } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..2d624381e67 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,18 +1,27 @@ package seedu.address.model; import java.nio.file.Path; +import java.util.Optional; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; +import seedu.address.model.consultation.Consultation; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.student.Student; /** * The API of the Model component. */ public interface Model { - /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** {@code Predicate} that always evaluate to true for students */ + Predicate PREDICATE_SHOW_ALL_STUDENTS = unused -> true; + + /** {@code Predicate} that always evaluate to true for consultations*/ + Predicate PREDICATE_SHOW_ALL_CONSULTATIONS = unused -> true; + + /** {@code Predicate} that always evaluate to true for lessons */ + Predicate PREDICATE_SHOW_ALL_LESSONS = unused -> true; /** * Replaces user prefs data with the data in {@code userPrefs}. @@ -53,35 +62,127 @@ public interface Model { ReadOnlyAddressBook getAddressBook(); /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a student with the same identity as {@code student} exists in + * the address book. */ - boolean hasPerson(Person person); + boolean hasStudent(Student student); /** - * Deletes the given person. - * The person must exist in the address book. + * Deletes the given student. + * The student must exist in the address book. */ - void deletePerson(Person target); + void deleteStudent(Student target); /** - * Adds the given person. - * {@code person} must not already exist in the address book. + * Adds the given student. + * {@code student} must not already exist in the address book. */ - void addPerson(Person person); + void addStudent(Student student); /** - * Replaces the given person {@code target} with {@code editedPerson}. + * Replaces the given student {@code target} with {@code editedStudent}. * {@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. + * The student identity of {@code editedStudent} must not be the same as + * another existing student in the address book. + */ + void setStudent(Student target, Student editedStudent); + + /** Returns an unmodifiable view of the filtered student list */ + ObservableList getFilteredStudentList(); + + /** + * Updates the filter of the filtered student list to filter by the given + * {@code predicate}. + * + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredStudentList(Predicate predicate); + + /** + * Returns true if a student with the same identity as {@code student} exists in + * the address book. + * Returns true if a consultation with the same details as {@code consult} + * exists in TAHub. + */ + boolean hasConsult(Consultation consult); + + /** + * Adds the given consult. + * + * @param consult Consultation to be added. + */ + void addConsult(Consultation consult); + + /** + * Replaces the given Consultation {@code target} with {@code editedConsult}. + * {@code target} must exist in TAHub. + * + * @param target Target consultation to replace. + * @param editedConsult Consultation instance to replace the target with. + */ + void setConsult(Consultation target, Consultation editedConsult); + + /** Returns an unmodifiable view of the filtered consultation list */ + ObservableList getFilteredConsultationList(); + + /** + * Updates the filter of the filtered consultation list to filter by the given + * {@code predicate}. + * + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredConsultationList(Predicate predicate); + + /** + * Deletes the given consultation. + * The consultation must exist in TAHub. + */ + void deleteConsult(Consultation consult); + + /** + * Finds a student by their name. + * + * @param name The name of the student to search for. + * @return An Optional containing the student if found, or empty if not. + */ + Optional findStudentByName(seedu.address.model.student.Name name); + + /** + * Returns true if a lesson with the same details as {@code lesson} exists in + * TAHub. + */ + boolean hasLesson(Lesson lesson); + + /** + * Adds the given lesson. + * + * @param lesson Lesson to be added. + */ + void addLesson(Lesson lesson); + + /** + * Deletes the given lesson. + * The lesson must exist in TAHub. + */ + void deleteLesson(Lesson lesson); + + /** + * Replaces the given Lesson {@code target} with {@code editedLesson}. + * {@code target} must exist in TAHub. + * + * @param target Target Lesson to replace. + * @param editedLesson Lesson instance to replace the target with. */ - void setPerson(Person target, Person editedPerson); + void setLesson(Lesson target, Lesson editedLesson); - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered lesson list */ + ObservableList getFilteredLessonList(); /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. + * Updates the filter of the filtered lesson list to filter by the given + * {@code predicate}. + * * @throws NullPointerException if {@code predicate} is null. */ - void updateFilteredPersonList(Predicate predicate); + void updateFilteredLessonList(Predicate predicate); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..97884e36d10 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,6 +4,7 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Optional; import java.util.function.Predicate; import java.util.logging.Logger; @@ -11,7 +12,10 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; +import seedu.address.model.consultation.Consultation; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.student.Name; +import seedu.address.model.student.Student; /** * Represents the in-memory model of the address book data. @@ -21,7 +25,9 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; - private final FilteredList filteredPersons; + private final FilteredList filteredStudents; + private final FilteredList filteredConsultations; + private final FilteredList filteredLessons; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -33,14 +39,17 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredStudents = new FilteredList<>(this.addressBook.getStudentList()); + filteredConsultations = new FilteredList<>(this.addressBook.getConsultList()); + filteredLessons = new FilteredList<>(this.addressBook.getLessonList()); } public ModelManager() { this(new AddressBook(), new UserPrefs()); } - //=========== UserPrefs ================================================================================== + // =========== UserPrefs + // ================================================================================== @Override public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { @@ -75,7 +84,8 @@ public void setAddressBookFilePath(Path addressBookFilePath) { userPrefs.setAddressBookFilePath(addressBookFilePath); } - //=========== AddressBook ================================================================================ + // =========== AddressBook + // ================================================================================ @Override public void setAddressBook(ReadOnlyAddressBook addressBook) { @@ -88,44 +98,127 @@ public ReadOnlyAddressBook getAddressBook() { } @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return addressBook.hasPerson(person); + public boolean hasStudent(Student student) { + requireNonNull(student); + return addressBook.hasStudent(student); } @Override - public void deletePerson(Person target) { - addressBook.removePerson(target); + public void deleteStudent(Student target) { + addressBook.removeStudent(target); } @Override - public void addPerson(Person person) { - addressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + public void addStudent(Student student) { + addressBook.addStudent(student); + updateFilteredStudentList(PREDICATE_SHOW_ALL_STUDENTS); } @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); + public void setStudent(Student target, Student editedStudent) { + requireAllNonNull(target, editedStudent); + addressBook.setStudent(target, editedStudent); + } + + // =========== Filtered Student List Accessors + // ============================================================= - addressBook.setPerson(target, editedPerson); + @Override + public ObservableList getFilteredStudentList() { + return filteredStudents; } - //=========== Filtered Person List Accessors ============================================================= + @Override + public void updateFilteredStudentList(Predicate predicate) { + requireNonNull(predicate); + filteredStudents.setPredicate(predicate); + } + + // =========== Filtered Consultation List Accessors + // ============================================================= /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of + * Returns an unmodifiable view of the list of {@code Consultation} backed by + * the internal list of * {@code versionedAddressBook} */ @Override - public ObservableList getFilteredPersonList() { - return filteredPersons; + public ObservableList getFilteredConsultationList() { + return filteredConsultations; + } + + @Override + public void updateFilteredConsultationList(Predicate predicate) { + requireNonNull(predicate); + filteredConsultations.setPredicate(predicate); + } + + // ========== Consultation Commands ========== + + @Override + public void addConsult(Consultation consult) { + addressBook.addConsult(consult); + updateFilteredConsultationList(PREDICATE_SHOW_ALL_CONSULTATIONS); + } + + @Override + public void setConsult(Consultation target, Consultation newConsult) { + requireAllNonNull(target, newConsult); + addressBook.setConsult(target, newConsult); + } + + @Override + public boolean hasConsult(Consultation consult) { + return addressBook.hasConsult(consult); + } + + @Override + public Optional findStudentByName(Name name) { + requireNonNull(name); + return filteredStudents.stream() + .filter(student -> student.getName().equals(name)) + .findFirst(); // Find and return the student by name + } + + @Override + public void deleteConsult(Consultation consult) { + addressBook.removeConsult(consult); + } + + // ========== Lesson Commands ========== + + @Override + public boolean hasLesson(Lesson lesson) { + requireNonNull(lesson); + return addressBook.hasLesson(lesson); + } + + @Override + public void addLesson(Lesson lesson) { + addressBook.addLesson(lesson); + updateFilteredLessonList(PREDICATE_SHOW_ALL_LESSONS); + } + + @Override + public void setLesson(Lesson target, Lesson newLesson) { + requireAllNonNull(target, newLesson); + addressBook.setLesson(target, newLesson); } @Override - public void updateFilteredPersonList(Predicate predicate) { + public void deleteLesson(Lesson lesson) { + addressBook.removeLesson(lesson); + } + + @Override + public ObservableList getFilteredLessonList() { + return filteredLessons; + } + + @Override + public void updateFilteredLessonList(Predicate predicate) { requireNonNull(predicate); - filteredPersons.setPredicate(predicate); + filteredLessons.setPredicate(predicate); } @Override @@ -134,7 +227,6 @@ public boolean equals(Object other) { return true; } - // instanceof handles nulls if (!(other instanceof ModelManager)) { return false; } @@ -142,7 +234,8 @@ public boolean equals(Object other) { ModelManager otherModelManager = (ModelManager) other; return addressBook.equals(otherModelManager.addressBook) && userPrefs.equals(otherModelManager.userPrefs) - && filteredPersons.equals(otherModelManager.filteredPersons); + && filteredStudents.equals(otherModelManager.filteredStudents) + && filteredConsultations.equals(otherModelManager.filteredConsultations) + && filteredLessons.equals(otherModelManager.filteredLessons); } - } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..0726eb0d641 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,7 +1,9 @@ package seedu.address.model; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; +import seedu.address.model.consultation.Consultation; +import seedu.address.model.lesson.Lesson; +import seedu.address.model.student.Student; /** * Unmodifiable view of an address book @@ -9,9 +11,20 @@ public interface ReadOnlyAddressBook { /** - * Returns an unmodifiable view of the persons list. - * This list will not contain any duplicate persons. + * Returns an unmodifiable view of the students list. + * This list will not contain any duplicate students. */ - ObservableList getPersonList(); + ObservableList getStudentList(); + /** + * Returns an unmodifiable view of the consults list. + * This list will not contain any duplicate students. + */ + ObservableList getConsultList(); + + /** + * Returns an unmodifiable view of the lessons list. + * This list will not contain any duplicate lessons. + */ + ObservableList getLessonList(); } diff --git a/src/main/java/seedu/address/model/consultation/Consultation.java b/src/main/java/seedu/address/model/consultation/Consultation.java new file mode 100644 index 00000000000..7b82f6cbd2a --- /dev/null +++ b/src/main/java/seedu/address/model/consultation/Consultation.java @@ -0,0 +1,192 @@ +package seedu.address.model.consultation; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import seedu.address.model.datetime.Date; +import seedu.address.model.datetime.Time; +import seedu.address.model.student.Student; +import seedu.address.model.student.exceptions.DuplicateStudentException; + +/** + * Represents a Consultation in the system. + * Guarantees: details are present and not null, field values are validated. + */ +public class Consultation { + + private final Date date; + private final Time time; + private final List students; + + /** + * Constructs a {@code Consultation}. + * + * @param date The date of the consultation. + * @param time The time of the consultation. + * @param students A list of students attending the consultation. + * This list can be empty but must not be null. + * @throws NullPointerException if {@code date} or {@code time} is null. + */ + public Consultation(Date date, Time time, List students) { + requireAllNonNull(date, time); + this.date = date; + this.time = time; + + this.students = students != null ? new ArrayList<>(students) : new ArrayList<>(); + } + + /** + * Constructs a copy of the given consultation. + * Creates new instances of date, time and the student list (not the students) to + * reduce the risk of accidental mutation. + * + * @param consultation The consultation to copy. + */ + public Consultation(Consultation consultation) { + requireNonNull(consultation); + this.date = new Date(consultation.getDate().getValue()); + this.time = new Time(consultation.getTime().getValue()); + this.students = new ArrayList<>(consultation.getStudents()); + } + + /** + * Returns the date of the consultation. + * + * @return The date of the consultation. + */ + public Date getDate() { + return date; + } + + /** + * Returns the time of the consultation. + * + * @return The time of the consultation. + */ + public Time getTime() { + return time; + } + + /** + * Returns an immutable list of students attending the consultation. + * + * @return A list of students attending the consultation. + * @throws UnsupportedOperationException if an attempt is made to modify the returned list. + */ + public List getStudents() { + return Collections.unmodifiableList(students); + } + + /** + * Adds a student to the consultation. + * + * @param student The student to add. + */ + public void addStudent(Student student) { + if (hasStudent(student)) { + throw new DuplicateStudentException(); + } + students.add(student); + } + + /** + * Removes a student from the consultation. + * + * @param student The student to remove. + */ + public void removeStudent(Student student) { + students.remove(student); + } + + /** + * Sets a student in the consultation. + * Method works by removing a Student & Adding a new Student. + * + * @param target The student to remove. + * @param editedStudent The student to add. + */ + public void setStudent(Student target, Student editedStudent) { + removeStudent(target); + addStudent(editedStudent); + } + + /** + * Returns true if the consultation contains the specified student. + * + * @param student The student to check for. + * @return True if the student is attending the consultation, false otherwise. + */ + public boolean hasStudent(Student student) { + return students.contains(student); + } + + /** + * Returns true if both consultations have the same date, time, and students. + * + * @param other The other consultation to compare. + * @return True if both consultations are the same, false otherwise. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Consultation)) { + return false; + } + + Consultation otherConsultation = (Consultation) other; + return date.equals(otherConsultation.date) + && time.equals(otherConsultation.time) + && students.equals(otherConsultation.students); + } + + @Override + public int hashCode() { + return Objects.hash(date, time, students); + } + + @Override + public String toString() { + return String.format("Consultation[date=%s, time=%s, students=%s]", date, time, students); + } + + /** + * Ensures that none of the arguments passed to the constructor are null, except the student list can be empty. + * + * @param objects The objects to check for null values. + * @throws NullPointerException if any object is null. + */ + private void requireAllNonNull(Object... objects) { + for (Object obj : objects) { + if (obj == null) { + throw new NullPointerException("Fields date and time must be non-null"); + } + } + } + + /** + * Returns true if both consultations have the same date and time. + * This defines a weaker notion of equality between two consultations compared to {@code equals}. + * + * @param otherConsultation The other consultation to compare to. + * @return True if the consultations have the same date and time, false otherwise. + */ + public boolean isSameConsultation(Consultation otherConsultation) { + if (otherConsultation == this) { + return true; + } + + if (otherConsultation == null) { + return false; + } + + return date.equals(otherConsultation.date) + && time.equals(otherConsultation.time); + } +} diff --git a/src/main/java/seedu/address/model/consultation/UniqueConsultList.java b/src/main/java/seedu/address/model/consultation/UniqueConsultList.java new file mode 100644 index 00000000000..5be94faeea3 --- /dev/null +++ b/src/main/java/seedu/address/model/consultation/UniqueConsultList.java @@ -0,0 +1,152 @@ +package seedu.address.model.consultation; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.consultation.exceptions.ConsultationNotFoundException; +import seedu.address.model.consultation.exceptions.DuplicateConsultationException; + +/** + * A list of consultations that enforces uniqueness between its elements and does not allow nulls. + * A consultation is considered unique by comparing using {@code Consultation#isSameConsultation(Consultation)}. + */ +public class UniqueConsultList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent consultation as the given argument. + */ + public boolean contains(Consultation toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameConsultation); + } + + /** + * Adds a consultation to the list. + * The consultation must not already exist in the list. + */ + public void add(Consultation toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateConsultationException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the consultation {@code target} in the list with {@code editedConsult}. + * {@code target} must exist in the list. + * The consultation identity of {@code editedConsult} + * must not be the same as another existing consultation in the list. + */ + public void setConsult(Consultation target, Consultation editedConsult) { + requireAllNonNull(target, editedConsult); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new ConsultationNotFoundException(); + } + + if (!target.isSameConsultation(editedConsult) && contains(editedConsult)) { + throw new DuplicateConsultationException(); + } + + internalList.set(index, editedConsult); + } + + /** + * Removes the equivalent consultation from the list. + * The consultation must exist in the list. + */ + public void remove(Consultation toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new ConsultationNotFoundException(); + } + } + + public void setConsults(UniqueConsultList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code consultations}. + * {@code consultations} must not contain duplicate consultations. + */ + public void setConsults(List consultations) { + requireAllNonNull(consultations); + if (!consultsAreUnique(consultations)) { + throw new DuplicateConsultationException(); + } + + internalList.setAll(consultations); + } + + /** + * 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) { + return other == this // short circuit if same object + || (other instanceof UniqueConsultList // instanceof handles nulls + && internalList.equals(((UniqueConsultList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code consultations} contains only unique consultations. + */ + private boolean consultsAreUnique(List consultations) { + for (int i = 0; i < consultations.size() - 1; i++) { + for (int j = i + 1; j < consultations.size(); j++) { + if (consultations.get(i).isSameConsultation(consultations.get(j))) { + return false; + } + } + } + return true; + } + + /** + * Returns a list of consultations that match the given predicate. + */ + public List filtered(Predicate predicate) { + requireNonNull(predicate); + return internalList.stream().filter(predicate).collect(Collectors.toList()); + } + + /** + * Sorts the list of consultations by date first, and then by time if the dates are the same. + * This method ensures that the consultations are ordered chronologically within the list. + * The list is sorted in-place, meaning the order of elements in the internal list is modified. + */ + public void sort() { + internalList.sort(Comparator.comparing(Consultation::getDate) + .thenComparing(Consultation::getTime)); + } +} diff --git a/src/main/java/seedu/address/model/consultation/exceptions/ConsultationNotFoundException.java b/src/main/java/seedu/address/model/consultation/exceptions/ConsultationNotFoundException.java new file mode 100644 index 00000000000..d2b5ab08113 --- /dev/null +++ b/src/main/java/seedu/address/model/consultation/exceptions/ConsultationNotFoundException.java @@ -0,0 +1,10 @@ +package seedu.address.model.consultation.exceptions; + +/** + * Signals that the specified consultation could not be found in the list. + */ +public class ConsultationNotFoundException extends RuntimeException { + public ConsultationNotFoundException() { + super("Consultation not found in the list"); + } +} diff --git a/src/main/java/seedu/address/model/consultation/exceptions/DuplicateConsultationException.java b/src/main/java/seedu/address/model/consultation/exceptions/DuplicateConsultationException.java new file mode 100644 index 00000000000..58ce7c7a632 --- /dev/null +++ b/src/main/java/seedu/address/model/consultation/exceptions/DuplicateConsultationException.java @@ -0,0 +1,11 @@ +package seedu.address.model.consultation.exceptions; + +/** + * Signals that the operation will result in duplicate Consultations + * (Consultations are considered duplicates if they have the same identity). + */ +public class DuplicateConsultationException extends RuntimeException { + public DuplicateConsultationException() { + super("Operation would result in duplicate consultations"); + } +} diff --git a/src/main/java/seedu/address/model/course/Course.java b/src/main/java/seedu/address/model/course/Course.java new file mode 100644 index 00000000000..b787ed54362 --- /dev/null +++ b/src/main/java/seedu/address/model/course/Course.java @@ -0,0 +1,62 @@ +package seedu.address.model.course; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Student's course in the system. + * Guarantees: immutable; is valid as declared in {@link #isValidCourse(String)} + */ +public class Course { + public static final String MESSAGE_CONSTRAINTS = "Courses should be in the format of two to four letters " + + "followed by four digits, followed by 0-2 letters: e.g., MA1100, GEA1000N, GESS1000T etc."; + /* + * The course code must follow the format specified in MESSAGE_CONSTRAINTS as shown above. + */ + public static final String VALIDATION_REGEX = "[a-zA-Z]{2,4}\\d{4}[a-zA-Z]{0,2}"; + + public final String courseCode; + + /** + * Constructs a {@code Course} and converts the given courseCode into uppercase. + * + * @param courseCode A valid course code, case-insensitive. + */ + public Course(String courseCode) { + requireNonNull(courseCode); + checkArgument(isValidCourse(courseCode), MESSAGE_CONSTRAINTS); + this.courseCode = courseCode.toUpperCase(); + } + + /** + * Returns true if a given string is a valid course code. + */ + public static boolean isValidCourse(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return courseCode; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Course)) { + return false; + } + + Course otherCourse = (Course) other; + return courseCode.equals(otherCourse.courseCode); + } + + @Override + public int hashCode() { + return courseCode.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/datetime/Date.java b/src/main/java/seedu/address/model/datetime/Date.java new file mode 100644 index 00000000000..2a61d8c6157 --- /dev/null +++ b/src/main/java/seedu/address/model/datetime/Date.java @@ -0,0 +1,106 @@ +package seedu.address.model.datetime; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.Objects; + +/** + * Represents a Date in the system. + * Guarantees: immutable; is valid as declared in {@link #isValidDate(String)}. + */ +public class Date implements Comparable { + + public static final String MESSAGE_CONSTRAINTS = "Dates should be in the format YYYY-MM-DD, " + + "and must be a valid date (e.g., no month 13 or day 32)."; + + private final String value; + + /** + * Constructs a {@code Date}. + * + * @param date A valid date string. + * @throws IllegalArgumentException if the given {@code date} is not a valid date. + */ + public Date(String date) { + if (!isValidDate(date)) { + throw new IllegalArgumentException(MESSAGE_CONSTRAINTS); + } + this.value = date; + } + + /** + * Returns the value of the date as a string. + * + * @return The string representation of the date. + */ + public String getValue() { + return value; + } + + /** + * Returns the LocalDate of String value. + * + * @return LocalDate of the date. + */ + public LocalDate getLocalDateValue() { + assert !value.isEmpty(); + return LocalDate.parse(value, DateTimeFormatter.ofPattern("uuuu-MM-dd")); + } + + /** + * Returns true if a given string is a valid date format (UUUU-MM-DD) and represents a real date. + * + * @param test The string to test for validity. + * @return True if the string represents a valid date, false otherwise. + */ + public static boolean isValidDate(String test) { + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd") + .withResolverStyle(ResolverStyle.STRICT);; + + try { + LocalDate.parse(test, dateFormatter); + return true; + } catch (DateTimeParseException e) { + return false; + } + } + + /** + * Compares this date with another date for ordering. + * Returns a negative integer, zero, or a positive integer as this date is before, equal to, + * or after the specified date. + * + * @param otherDate The date to be compared. + * @return A negative integer, zero, or a positive integer as this date is less than, equal to, + * or greater than the specified date. + */ + public int compareTo(Date otherDate) { + return this.value.compareTo(otherDate.value); // Compare LocalDate objects + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Date)) { + return false; + } + + Date otherDate = (Date) other; + return value.equals(otherDate.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/seedu/address/model/datetime/Time.java b/src/main/java/seedu/address/model/datetime/Time.java new file mode 100644 index 00000000000..9b352f62992 --- /dev/null +++ b/src/main/java/seedu/address/model/datetime/Time.java @@ -0,0 +1,104 @@ +package seedu.address.model.datetime; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.Objects; + +/** + * Represents a Time in the system. + * Guarantees: immutable; is valid as declared in {@link #isValidTime(String)}. + */ +public class Time implements Comparable