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/.gitignore b/.gitignore index 284c4ca7cd9..a6150c3d767 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ +archived diff --git a/README.md b/README.md index 16208adb9b6..85292f01e14 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![Java CI](https://github.com/AY2425S1-CS2103T-W10-4/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2425S1-CS2103T-W10-4/tp/actions/workflows/gradle.yml)[![codecov](https://codecov.io/gh/AY2425S1-CS2103T-W10-4/tp/graph/badge.svg?token=ISBALRKKO7)](https://codecov.io/gh/AY2425S1-CS2103T-W10-4/tp) + +# StoreClass +**StoreClass** is a project done by Software Engineering (SE) students.
![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. +## What is StoreClass? +* StoreClass is designed to be used by educators in private educational institutions such as tuition centers, allowing educators to quickly +access student information such as contact details, class details and other necessary information. +* It is optimized for users who are comfortable using the command-line interface (CLI), which allows for faster inputs through text-based inputs. + +## Project Information * 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. +* It is named `StoreClass` as it is a play on words, stating how the class of students are stored. +* For the detailed documentation of this project, see the **[StoreClass Documentation](https://ay2425s1-cs2103t-w10-4.github.io/tp/)**. +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). diff --git a/build.gradle b/build.gradle index 0db3743584e..8c29cd2be20 100644 --- a/build.gradle +++ b/build.gradle @@ -70,3 +70,7 @@ shadowJar { } defaultTasks 'clean', 'test' + +run { + enableAssertions = true +} diff --git a/docs/AboutUs.md b/docs/AboutUs.md index ff3f04abd02..0cf61b90f70 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,61 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Prabhu NATARAJAN - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] +[[homepage](https://www.comp.nus.edu.sg/cs/people/prabhu/)] +[[github](https://github.com/prabhu-na)] [[portfolio](team/johndoe.md)] * Role: Project Advisor -### Jane Doe +### Tan Yong Quan - + -[[github](http://github.com/johndoe)] +[[github](http://github.com/yongqqqq)] [[portfolio](team/johndoe.md)] * Role: Team Lead * Responsibilities: UI -### Johnny Doe +### Isaac Lim Tzee Zac - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](https://github.com/isaactodo)] [[portfolio](team/johndoe.md)] -* Role: Developer -* Responsibilities: Data +* Role: Documentation, Code Quality, Deliverables and Deadlines +* Responsibilities: Integration -### Jean Doe +### Lei Jianwen - + -[[github](http://github.com/johndoe)] +[[github](http://github.com/jianwen0451)] [[portfolio](team/johndoe.md)] -* Role: Developer +* Role: Developer Java Expert * Responsibilities: Dev Ops + Threading -### James Doe +### Collin Tan Yu Qi - + -[[github](http://github.com/johndoe)] +[[github](http://github.com/tanyqcollin)] [[portfolio](team/johndoe.md)] * Role: Developer * Responsibilities: UI + +### Tee Jing Hong + + + +[[github](http://github.com/RadieonAjax)] +[[portfolio](team/RadieonAjax.md)] + +* Role: Developer +* Responsibilities: UI diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 743c65a49d2..a6a87e303c9 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,6 +2,9 @@ layout: page title: Developer Guide --- + +![Logo](images/StoreClass-Logo.png) + * Table of Contents {:toc} @@ -72,7 +75,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`, `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. However, `DialogBox` inherits from `HBox`. 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) @@ -83,6 +86,7 @@ The `UI` component, * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. * depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +`CommandBox` holds a reference towards `AutocompleteParser` which helps parses user input for a list of suggestions to show to the user. ### Logic component **API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) @@ -144,6 +148,25 @@ The `Storage` component, * can save both address book data and user preference data in JSON format, and read them back into corresponding objects. * inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). * depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) + +The following is an example JSON output for a person: + +```json +{ + "name": "John Doe", + "phone": "98765432", + "gender": "male", + "modules": [ + { + "module": "CS2103T", + "grade": 85 + } + ], + "tags": [ + "colleague" + ] +} +``` ### Common classes @@ -155,37 +178,75 @@ Classes used by multiple components are in the `seedu.address.commons` package. This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### Undo/redo feature + +#### Implementation Overview + +The undo/redo functionality is implemented in the `VersionedAddressBook`, which extends the basic `AddressBook` to include versioning capabilities. This is achieved by maintaining an internal history of address book states. The history is stored in a list (`addressBookStateList`), and the current state is tracked by the `currentStatePointer`. + +The key operations involved in undo/redo are: -#### Proposed Implementation +- **`VersionedAddressBook#save()`**: Saves the current state of the address book into the history list. +- **`VersionedAddressBook#undo()`**: Restores the address book to the previous state. +- **`VersionedAddressBook#redo()`**: Restores the address book to a previously undone state. -The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: +These operations are available to the rest of the system through the `Model` interface, specifically: -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +- `Model#saveAddressBook()` +- `Model#undoAddressBook()` +- `Model#redoAddressBook()` -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +--- + +## How Undo/Redo Works + +The mechanism functions as follows: -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +1. **Save**: Every modification to the address book, such as `add`, `delete`, or `edit` commands, triggers a save operation to save the current state of the address book. -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +2. **Undo**: The undo operation moves the `currentStatePointer` backward and restores the previous state. If no more undo operations can be performed (i.e., if the pointer is at the start of the history), the operation fails and returns an error. + +3. **Redo**: The redo operation moves the `currentStatePointer` forward and restores the next state that was undone. If there are no undone states, the operation fails and returns an error. + +--- + +### Example Flow + +Here is how undo and redo would work through a sequence of user actions: + +--- + +#### Step 1. Initial State +Upon launching the application, the `VersionedAddressBook` is initialized with the default state (an empty address book, for example). This state is stored in the `addressBookStateList` as the first entry, and the `currentStatePointer` points to this state. ![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. Performing a Deletion +When a user executes the `delete` command (e.g., `delete 5`), the current state of the address book is saved before the change is made. This ensures that the delete action is reversible. The new state of the address book (with the 5th person deleted) is then stored in the history. + +The `saveAddressBook()` operation is called internally, and the `currentStatePointer` is moved forward to point to the newly saved 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. Performing an Addition +Next, if a user executes an `add` command (e.g., `add n/David ...`), a new state of the address book is created. As before, `saveAddressBook()` is invoked to save the new state, and the `currentStatePointer` is shifted to this new state. ![UndoRedoState2](images/UndoRedoState2.png) -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +
:information_source: **Note:** If a command fails its execution, it will not call `Model#saveAddressBook()`, so the address book state will not be saved into the `addressBookStateList`.
-Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +--- + +#### Step 4. Undoing the Addition +If the user decides to undo the most recent action (the `add` command), the `undo` operation will restore the address book to the state before the addition. Internally, `undoAddressBook()` is called, which moves the `currentStatePointer` backward and restores the state. + +If the pointer is already at the first state (the initial state), attempting to undo will return an error. ![UndoRedoState3](images/UndoRedoState3.png) @@ -194,7 +255,10 @@ than attempting to perform the undo.
-The following sequence diagram shows how an undo operation goes through the `Logic` component: +#### Undo Sequence Diagram +The following sequence diagram shows how the undo operation works across the `Logic` and `Model` components: + +`Logic`: ![UndoSequenceDiagram](images/UndoSequenceDiagram-Logic.png) @@ -202,7 +266,7 @@ The following sequence diagram shows how an undo operation goes through the `Log -Similarly, how an undo operation goes through the `Model` component is shown below: +`Model`: ![UndoSequenceDiagram](images/UndoSequenceDiagram-Model.png) @@ -212,39 +276,91 @@ 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. Redoing the Addition +If the user decides to redo the action after undoing it, the `redo` operation will restore the state that was undone. The `redoAddressBook()` method is invoked, which moves the `currentStatePointer` forward to the next state in history and restores that state. + +As with undo, if the pointer is at the last state in the list, trying to redo will return an error. ![UndoRedoState4](images/UndoRedoState4.png) -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. +#### Redo Sequence Diagram +The following sequence diagram shows how the redo operation works across the `Logic` and `Model` components: ![UndoRedoState5](images/UndoRedoState5.png) +--- + The following activity diagram summarizes what happens when a user executes a new command: -#### Design considerations: -**Aspect: How undo & redo executes:** +--- -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +## Design Considerations -* **Alternative 2:** Individual command knows how to undo/redo by - itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. +Here are the important aspects of the undo/redo implementation: -_{more aspects and alternatives to be added}_ +- **Saving Mechanism:** Each modification to the address book (like `add`, `delete`, or `edit`) triggers a call to `saveAddressBook()`. This function stores the current state of the address book and updates the `currentStatePointer`. However, commands that do not modify the address book (such as `list`, `find`, or `filter`) do not trigger a call to `saveAddressBook()`, and therefore do not modify the history. -### \[Proposed\] Data archiving +- **Undo/Redo Pointer Management:** The `currentStatePointer` is moved backward during undo and forward during redo. If no more states can be undone or redone, appropriate error messages are returned. -_{Explain here how the data archiving feature will be implemented}_ +- **Memory Considerations:** Since each address book state is stored in memory, excessive changes could increase memory usage. The current implementation keeps track of all states but might need optimization in the future, depending on the application’s use case. +- **State Purging:** After calling `undo`, when a command that modifies the address book is executed (such as `add`, `delete`, or `edit`), the states after the current one are purged to ensure consistency. This prevents errors from conflicting history states. Additionally, after calls to `archive` and `load`, the history is also purged to ensure consistency between different data files. --------------------------------------------------------------------------------------------------------------------- +--- + + + +### Data archiving + +The archive and load feature is achieved through `ArchiveCommand` and `LoadCommand` which both extend the `Command` class. When such command is executed, the LogicManager will update the Storage when necessary. + +#### The Following UML Object Diagrams will illustrate how archive and load are done + +Before the archiving or loading + +We will use a simple case where there is one working AddressBook named `addressBook.json` in `data` folder and one archived AddressBook named `archivedFile1.json` in `archived` folder + +![ArchiveAndLoadInitialState](images/ArchiveAndLoadInitialState.png) + +**Scenario 1 Archive to a new file** + +In this scenario, the user is trying to archive the current address book into a new file named `archiveFile2.json`. He enters the command `archive pa/archiveFile2.json` A new file names `archiveFile2.json` will be created and hold the data of `addressBook.json`. And the data in `addressBook.json` will be discarded. + +![ArchiveToNewFile](images/ArchiveToNewFile.png) + +**Scenario 2 Archive to a existing file** + +In this scenario, the user is trying to archive the current address book into the existing file named `archiveFile1.json`. He enters the command `archive pa/archiveFile1.json` A file names `archiveFile1.json` will be overwritten and hold the data of `addressBook.json`. And the data in `addressBook.json` will be discarded. + +![ArchiveToNExistingFile](images/ArchiveToExistingFile.png) + +The following sequence diagram illustrate how an archive operation is processed under `Logic` component. + +![ArchiveSequenceDiagram](images/ArchiveSequenceDiagram-Logic.png) + +
:information_source: **Note:** The lifeline for `ArchiveCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. Some details like parameters of function is omitted for simplicity. + +
+ +**Scenario 3 Loading from a file** + +In this scenario, the user is trying to load the address book from a file named `archiveFile1.json`. He enters the command `load pa/archiveFile1.json`. The data in the current working address book will be discarded. The data in `archiveFile1.json` will be loaded into the working address book. +![Load](images/Load.png) + +The following sequence diagram illustrate how an archive operation is processed under `Logic` component. + +![LoadSequenceDiagram](images/LoadSequenceDiagram-Logic.png) + +
:information_source: **Note:** The lifeline for `LoadCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. Some details like parameters of function is omitted for simplicity. + +
+ +------------------------------------------------------------------------------- ## **Documentation, logging, testing, configuration, dev-ops** @@ -262,42 +378,64 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts +* a teacher in an educational institution (private institution, i.e. tuition centers) +* need to manage large amount of student information * prefer desktop apps over other types * can type fast * prefers typing to mouse interactions * is reasonably comfortable using CLI apps -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: It allows for easy and efficient retrieval or storage of student information while providing a clean and user-friendly interface. The application supports modularity, and users are able to import and export to other similar applications without relying on complex or costly software. ### User stories -Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` +Priorities: High (must have) - `* * *`, Medium (Good to have) - `* *`, Low (nice to have) - `*` + +| Priority | As a …​ | I want to …​ | So that I can…​ | +|----------|----------|-----------------------------------------------------------|----------------------------------------------------------------| +| `* * *` | educator | add students into the database | easily refer to their information when needed | +| `* * *` | educator | list all students to view the number of students | collate that information | +| `* * *` | educator | delete a student | remove entries that I no longer need | +| `* * *` | educator | find a person by name | find the relevant person without scrolling through a long list | +| `* *` | educator | clear all information | start anew for a new academic year | +| `* *` | educator | update details easily when there are changes | have the most updated information | +| `* *` | educator | categorize students into groups | | +| `* *` | educator | record students grades for tests and assignments | | +| `* *` | educator | view a summary of each student's grade | | +| `* *` | educator | tag students with relevant labels | prioritize students based on their status | +| `* *` | educator | record notes on student behaviour | easily track issues related to their behaviour | +| `* *` | educator | archive old student data | keep my AB clean while being able to retrieve old information | +| `*` | educator | set learning goals | track their progress towards these goals | +| `*` | educator | undo/redo any changes | avoid re-entering the data during a mis-entry | +| `*` | educator | see a list of suggested commands when typing out commands | easily type in the commands that I want and reduce typos | +| `*` | educator | export student data | share the information with others | +| `*` | educator | keep track of meetings with students | keep track of my commitments | +| `*` | educator | see sample data | try out the app's feature without adding my own student data | -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | -*{More to be added}* ### 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 `StoreClass` and the **Actor** is the `user`, unless specified otherwise) -**Use case: Delete a person** +#### **Use case 1: List out all students** -**MSS** +**Main Success Scenario** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User requests to list students +2. StoreClass shows a list of students + + Use case ends. + +#### **Use case 2: Delete a student** + +**Main Success Scenario** + +1. User requests to list students +2. StoreClass shows a list of students +3. User requests to delete a specific student in the list +4. StoreClass deletes the student Use case ends. @@ -313,20 +451,203 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli Use case resumes at step 2. -*{More to be added}* +#### **Use case 3: Add a student** + +**Main Success Scenario** + +1. User requests to add persons and type in the relevant information +2. StoreClass adds the new student +3. StoreClass show the new list of students + Use case ends. + +**Extensions** + +* 1a. The provided information is invalid. + + * 1a1. StoreClass displays the corresponding data error message. + + Use case resumes at step 1. + +#### **Use case 4: Search for student** + +**Main Success Scenario** +1. User requests to search for student with the relevant search query. +2. StoreClass displays the relevant student(s) matching the query.
+ Use case ends. + +**Extensions** + +* 1a. StoreClass is unable to find any matching results. + + * 1a1. StoreClass displays a message indicating that not students match the search query. + + Use case ends. + +#### **Use case 5: Update Student Information** + +**Main Success Scenario** + +1. User requests to list students (UC1). +2. User selects the specific student from the list to update. +3. User enters the new information. +4. StoreClass updates the student's details and display a success message.
+ Use case ends. + +**Extensions** + +* 4a. StoreClass detects an error in the entered information. + + * 1a1. StoreClass displays a message indicating which fields are invalid. + + Use case ends. + +#### **Use case 6: Record Student Grades** + +**Main Success Scenario** + +1. User requests to list students (UC1). +2. User selects the specific student to record a grade. +3. User enters the grade information. +4. StoreClass updates the student's grades and display a success message.
+ Use case ends. + +**Extensions** + +* 4a. StoreClass detects an error in the entered information. + + * 1a1. StoreClass displays a message indicating which fields are invalid. + + Use case ends. + +#### **Use case 7: Categorize Students** + +**Main Success Scenario** + +1. User requests to list students (UC1). +2. User selects one or more students to be categorized. +3. StoreClass requests for the group to assign the students to. +4. User selects the group. +5. StoreClass categorize the student(s) and display a success message.
+ Use case ends. + +**Extensions** + +* 4a. StoreClass detects that no groups exist. + + * 4a1. StoreClass allows the user to create a new group. + * 4a2. StoresClass creates a new group. + + Use case resumes from step 6. + +#### **Use case 8: Tag Students** + +**Main Success Scenario** + +1. User requests to list students (UC1). +2. User selects one or more students to be tagged. +3. StoreClass requests for the tag(s) to assign the students to. +4. User enters the tag(s) +5. StoreClass applies the tags to the selected student(s) and display a success message.
+ Use case ends. + +**Extensions** + +* 4a. StoreClass detects an invalid tag(s). + + * 4a1. StoreClass requests a valid tag. + * 4a2. User enters a valid tag.
+ Steps 4a1-4a2 are repeated until a valid tag is entered.
+ Use case resumes from step 6. + +#### **Use case 9: Archive Student Data** + +**Main Success Scenario** + +1. User requests to archive student data. +2. StoreClass requests confirmation for archiving. +3. User confirms. +4. StoreClass archives the students data and removes them from the current interface. +5. StoreClass displays a success message.
+ Use case ends. + +**Extensions** + +* *a. User wishes to view the archived data. + + * *a1. StoreClass lists all available archives. + * *a2. User selects the archive. + * *a3. StoreClass displays the archives information. + + Use case ends + +#### **Use case 10: Export Student Data** + +**Main Success Scenario** + +1. User chooses to export student data. +2. StoreClass requests confirmation for exporting. +3. User confirms. +4. StoreClass exports the students data and displays a success message.
+ Use case ends. + +**Extensions** + +* *4a. StoreClass is unable to export the data. + + * *4a1. StoreClass returns an error message. + + Use case ends. + +#### **Use case 11: Undo/Redo Actions** + +**Main Success Scenario** + +1. User performs an action/command. +2. User chooses to undo the action. +3. StoreClass reverses the action and displays a success message.
+ Use case ends. + +**Extensions** + +* *a. User chooses to redo the action. + + * *a1. StoreClass restores the previous action and display a success message. + + Use case ends. + +#### **Use case 12: Filter Student List** + +**Main Success Scenario** + +1. User request to filter a list of student based on specified conditions. +2. StoreClass displays all students that match all conditions.
+ Use case ends. + +**Extensions** +* 1a. No matching student found. + * 1a1. StoreClass display a message indicating that no students match the search query.
+ Use case ends. + ### Non-Functional Requirements 1. Should work on any _mainstream OS_ as long as it has Java `17` or above installed. 2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. 3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +4. The product should be an offline product that can run without accessing the internet, allowing access to core features such as adding, deleting, listing and archiving. +5. Sensitive student data such as contact details, grades and payment information should be protected through encryption or password protection to prevent unauthorized access. +6. The system should be able to manage an increasing number of students and additional data fields without significant performance degradation, ensuring that response times remains under 200ms. -*{More to be added}* ### Glossary -* **Mainstream OS**: Windows, Linux, Unix, MacOS +* **Archive**: A feature that allows users to store old data for use later without cluttering the current interface. +* **Export**: Saving the student data in a file format such as `.csv` or `.txt` for external use. * **Private contact detail**: A contact detail that is not meant to be shared with others +* **Mainstream OS**: Windows, Linux, Unix, MacOS +* **Student Number**: A unique identifier assigned to each student. +* **Tag**: A label that can be added to a student for categorization or searching. +* **Undo/Redo**: The ability to reverse an action/command made in the application. -------------------------------------------------------------------------------------------------------------------- diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index bac5eb36d35..df2c6a74fbd 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -216,6 +216,8 @@ GEM nokogiri (1.16.5) mini_portile2 (~> 2.8.2) racc (~> 1.4) + nokogiri (1.16.5-x64-mingw32) + racc (~> 1.4) octokit (4.25.1) faraday (>= 1, < 3) sawyer (~> 0.9) @@ -254,6 +256,7 @@ GEM unf_ext (0.0.8.2) unf_ext (0.0.8.2-x64-mingw32) unicode-display_width (1.8.0) + wdm (0.1.1) webrick (1.8.1) PLATFORMS @@ -263,7 +266,8 @@ PLATFORMS DEPENDENCIES github-pages jekyll + wdm (~> 0.1.0) webrick BUNDLED WITH - 2.1.4 + 2.2.33 diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 84b4ddc4e40..9d98ce44ce0 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,8 +3,14 @@ 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. +![Logo](images/StoreClass-Logo.png) +Tired of using paper or Microsoft Excel to track your student details? No worries, StoreClass is here to help! + +StoreClass (SC) is a desktop app designed exclusively for teachers from private organizations e.g. tuition centers to manage their students. As teachers, you can interact with the app by keying in your commands to StoreClass's chat bot, and your student details will be updated and saved automatically. + +Paired also with a revolutionary AutoComplete feature, StoreClass helps you manage your student details more effectively, efficiently and most importantly, reliably. +## Table of Contents * Table of Contents {:toc} @@ -12,30 +18,41 @@ 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. Before you use our program, ensure you have Java `17` or above installed in your Computer. + - If you are using Windows 11, see [here](https://www.youtube.com/watch?v=ykAhL1IoQUM) for an installation guide. + - If you are using MacOS, see [here](https://www.youtube.com/watch?v=lYKHFz8YaD4) instead. + +
+ :question: **Why do I need to install Java?** + Our program is coded in Java. So in order for you to use it, Java will need to be installed on your machine. Think of it as the engine for our program. +
+ -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 StoreClass. -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +2. Download the latest version of the `.jar` file of our program from [here](https://github.com/AY2425S1-CS2103T-W10-4/tp/releases). -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: +3. Copy the file to the folder you want to use as the _home folder_ for StoreClass. - * `list` : Lists all contacts. +4. Open the terminal on your computer and run the jar file. + - How do I run a jar file using Terminal? See [here](https://www.youtube.com/watch?v=j7A7DOZePXs) + - After you run the jar file, A GUI similar to below should appear on your screen in a few seconds. Note how the app contains some sample data.
+ ![Ui](images/Ui.png) - * `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. +5. Type a command of your choice in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
+ Here are some examples of commands you can try: - * `delete 3` : Deletes the 3rd contact shown in the current list. + * `add n/John Doe p/98765432 g/male m/Physics` : Adds a student named `John Doe` to StoreClass. - * `clear` : Deletes all contacts. + * `list` : Lists all students and their details. - * `exit` : Exits the app. + * `delete 3` : Deletes the 3rd student shown in the current list of students. -1. Refer to the [Features](#features) below for details of each command. + * `clear` : Deletes all students. + + * `exit` : Exits the program. +6. Refer to the [Features](#features) below for a more detailed overview of each command. -------------------------------------------------------------------------------------------------------------------- @@ -43,60 +60,74 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo
-**:information_source: Notes about the command format:**
+**:information_source: Before you read on, here are some important notes about the command format that you need to know:**
-* Words in `UPPER_CASE` are the parameters to be supplied by the user.
+* Words in `UPPER_CASE` are the parameters to be supplied by you.
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`. +* Items with `...`​ after them can be used multiple times.
+ e.g. `m/MODULE...​` can be used as `m/Mathematics`, `m/Biology m/Physics` etc. + * Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. + e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/paid_tuition`, `t/paid_tuition t/smart` etc. -* Parameters can be in any order.
+* Parameters can be in any order unless specified.
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.
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. +* If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines, as space characters surrounding line-breaks may be omitted when copied over to the application.
### Viewing help : `help` -Shows a message explaning how to access the help page. +You can view the help page using this command. ![help message](images/helpMessage.png) Format: `help` -### Adding a person: `add` +### Adding a student: `add` + +You can add a student to StoreClass using this command. -Adds a person to the address book. +Format: `add n/NAME p/PHONE_NUMBER g/GENDER m/MODULE... [t/TAG]…​` -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +* `n/NAME` : The full name of the student to be added. +* `p/PHONE_NUMBER` : The phone number of the student to be added. +* `g/GENDER` : The gender of the student to be added. +* `m/MODULE` : The module that the student is taking. +* `t/TAG` : The tag of the student. -
:bulb: **Tip:** -A person can have any number of tags (including 0) +
:notebook: **Note:** Field Constraints +- Names should only contain alphabets, hyphens, dots, commas, forward slash and spaces, and be between 1 and 100 characters long. +- Phone numbers should only contain numbers, and be exactly 8 digits long. +- Gender should be either `male` or `female`. +- Module should consist of alphanumeric characters and spaces only, and it should be between 1 and 30 characters long. +- Tag should consist of alphanumeric characters only, and it should be between 1 and 30 characters long.
Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* `add n/John Doe p/98765432 g/male m/Mathematics` : Adds a student named `John Doe` to StoreClass. +* `add n/Betsy Crowe g/female p/1234567 m/Physics m/Chemistry t/OLevels t/new` : Adds a student named `Betsy Crowe` to StoreClass. -### Listing all persons : `list` -Shows a list of all persons in the address book. +### Listing all students : `list` + +You can show a list of all students in StoreClass. Format: `list` -### Editing a person : `edit` +### Editing a student : `edit` -Edits an existing person in the address book. +You can edit an existing student in StoreClass. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Format: `edit INDEX [n/NAME] [p/PHONE] [g/GENDER] [m/MODULE]... [t/TAG]…​` * Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ * At least one of the optional fields must be provided. @@ -106,76 +137,309 @@ Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` specifying any tags after it. Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +* `edit 1 p/91234567` : Edits the phone number of the 1st person to be `91234567`. +* `edit 2 n/Betsy Crower t/` : Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. -### Locating persons by name: `find` +### Locating students by name: `find` -Finds persons whose names contain any of the given keywords. +You can find students whose names or tags 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 the name and tags are 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). +* Students matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) -### Deleting a person : `delete` +* `find John` returns `john` and `John Doe` _(search by name)_ +* `find colleague` returns `Bernice Yu` and `Roy Balakrishnan` _(search by tag)_ +* `find alex david` returns `Alex Yeoh`, `David Li` _(search by multiple parameters)_
+ +![result for 'find alex david'](images/findResult.png) + +### Filter students : `filter` -Deletes the specified person from the address book. +You can filter students who meet all specified conditions. + +Format: `filter [n/name] [p/phone] [g/gender] [t/tag]... [m/module]...` +* The filter is case-insensitive. eg `hans` will match `Hans`. +* At least one of the optional fields must be provided. +* Only full words will be matched e.g. `Han` will not match `Hans`, same to all parameter except phone number. +* At least 3 digits of number must be provided to filter phone number and it will return all matching numbers that contains specified number. +* Students matching all the given conditions will be returned (i.e. `AND` search). + +
⚠️ **Warning:** +Each parameter can only contain one keyword. +
+ +Examples: +* `filter n/John` : returns `john` and `John Doe` (filter by name) +* `filter g/male t/new` : returns `James Li`, `Roy Balakrishnan` and `Linus Koo`. _(filter by gender and tag)_ +* `filter g/female t/new t/OLevels` : returns `Alex Yeoh` and `David Li` _(filter by gender and multiple tags)_ +* `filter g/female t/IB m/Physics` : return `Bernice Yu` _(filter by multiple conditions)_ + +### Deleting a student : `delete` + +You can delete a specified student from StoreClass. Format: `delete INDEX` -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. +* 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, …​ 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. +### Undoing the last action: `undo` + +The `undo` command allows you to reverse the last action performed, helping you recover any data that may have been unintentionally deleted or modified. + +**Format:** `undo` + +#### How it works: +- When you perform an action that modifies the address book (like adding, editing, or deleting an entry), that action is saved in memory. +- By executing `undo`, the most recent modification is reverted, and the address book is restored to its previous state. + +
+:bulb: **Tips:** for Efficient Usage
+ +1. **Use Undo After a Mistake**: If you accidentally delete or modify a contact, you can quickly use `undo` to revert the last action and restore the previous state.
+2. **Undo Works Only for Modifying Commands**: Only actions that modify the address book (like `add`, `edit`, or `delete`) can be undone. Commands like `list`, `filter`, or `find` do not trigger the undo mechanism. The reasoning behind this is that we consider them as view commands, not action commands that alter student data. +
+ +
+:question: **Common Question:** +Why does `undo` not work after I run `list`, `filter`, or `find`?
+`undo` only works for actions that modify the address book. Since `list`, `filter`, or `find` do not modify the data, they do not impact the undo history. +
+ +
+:rotating_light: **Warning:** +Executing a command that modifies the address book (like `add`, `edit`, or `delete`) will **clear the redo stack**. This means once you undo an action and perform another modification, you cannot redo the previous undone action. +
+ +#### Example Scenario: +1. You delete a contact. + - *Before Delete:* [Initial State] + - *After Delete:* [After Delete Command] +2. You decide to undo the delete action. + - *After Undo:* [After Undo Command] – The deleted contact is restored. + +Initial State +After Delete Command +After Undo Command + +Examples: +* `undo` will revert the last command executed, restoring the previous state of the address book. + +### Redoing the Last Undone Action: `redo` + +The `redo` command allows you to reapply the last action that was undone, restoring the previous state of the address book. This can be useful if you change your mind after undoing an action. + +**Format:** `redo` + +- **Note:** Like `undo`, the `redo` command cannot be used with commands like `list`, `filter`, or `find`. + +#### How it works: +- If you’ve used `undo` to reverse an action, the `redo` command will restore that action, essentially “re-doing” it. + +
+:bulb: **Tips:** for Efficient Usage
+ +1. **Use Redo to Restore Actions**: If you’ve undone an action by mistake, `redo` lets you reapply the change quickly. It’s useful when you second-guess your decision. +2. **Redo Works Only After Undo**: `redo` will only work if an action has been undone previously. If you haven’t undone an action, `redo` will not perform anything. +
+ +
+:question: **Common Question:** +Why does `redo` not work after I’ve made new changes to the address book?
+`redo` only works if there is a previous action that was undone. If a new action is performed after an undo, the redo history is cleared, and there is nothing to redo. +
+ +
+:rotating_light: **Warning:** +Executing a command that modifies the address book (like `add`, `edit`, or `delete`) will **clear the redo stack**. This means once you undo a change and then modify the address book again, you will lose the ability to redo the previous undone action. +
+ +#### Example Scenario: +1. You undo a deletion of a contact. + - *After Undo:* [After Undo Command] – The deleted contact is restored. +2. You decide to redo the action and restore the contact again. + - *After Redo:* [After Redo Command] – The contact is deleted once more. + +After Redo Command + +--- + +### Grading a Module: `grade` + +You can assign a grade to a module that a student is taking. + +**Format:** `grade INDEX [m/MODULE s/GRADE]` + +- Assigns a numerical grade (between 0 and 100) to the module identified by the `INDEX` number shown in the displayed person list. +- `INDEX`: The index number of the student in the displayed person list (must be a positive integer). +- `m/MODULE`: The module code to which the grade is assigned. +- `s/GRADE`: The numerical grade (between 0 and 100) to assign to the module. +- You can provide multiple `m/MODULE s/GRADE` pairs to assign grades to multiple modules in a single `grade` command. +- The grade can be any whole number between 0 and 100, inclusive. + +
:notebook: **Important Note:** +- Each module specified in the `grade` command must be a module that the student is taking. +- The number of `m/MODULE` prefixes must match the number of `s/GRADE` prefixes. +- Grades are assigned to modules based on the order of the `m/MODULE s/GRADE` pairs provided in the command.
+ +
+:bulb: Tip: +You can hover over each individual module to view the grades for that module. +
+ +**Examples:** +- `grade 1 m/Physics s/85` : assigns a grade of 85 to Physics for the first student. +- `grade 2 m/Chemistry s/90` : assigns a grade of 90 to Chemistry for the second student. +- `grade 3 m/English s/80 m/Chinese s/85` assigns a grade of 80 to English and 85 to Chinese for the third student. + + +### Archiving data files `archive` + +You can archive the current address book to a specific file name. + +After archiving, you can find the archived file in the folder `archived` with the name `FILENAME` in the home folder that you've chosen to put the `jar` file of StoreClass in. + +Format: `archive pa/FILENAME` + +Example: `archive pa/mybook.json` + +The file name must end with ".json" and must not contain any slashes "/". + +There should be only one file name provided. + +
:rotating_light: **Warning:** +When you execute the archive command, all entries in the current StoreClass will be discarded. + +If you choose an existing archive file as the file name when archiving, the old archive file will be overwritten. +
+ +### Load data files `load` + +You can load data from an archived file into StoreClass. + +You can only load from files under the folder named `archived`. This folder is located in the home folder that you've chosen to put the `jar` file of StoreClass in. + +Format: `load pa/FILENAME` + +Example: `load pa/mybook.json` + +The file name must end with ".json", must not contain any slashes "/" and must point to an existing StoreClass .json file. + +There should be only one file name provided. + +
:rotating_light: **Warning:** +It is recommended that you avoid loading non-StoreClass .json files, as it may result in unexpected behaviors + +When you execute the load command, all the entries in the current StoreClass will be overwritten. So, it is recommended that you archive the current data in StoreClass before loading. +
+ +
+:question: **Common Question:** +What happens to the undo/redo stack after archiving or loading the address book?
+When you call `archive` or `load`, the undo/redo history is cleared to prevent inconsistencies between the address book data and the stored history. Always ensure the current state is saved before performing these actions. +
+ ### Clearing all entries : `clear` -Clears all entries from the address book. +You can all the current entries in StoreClass with this command. Format: `clear` ### Exiting the program : `exit` -Exits the program. +You can exit the program with this command. Format: `exit` +### Autocomplete + +The Autocomplete feature provides real-time command suggestions as you type, helping you quickly and accurately enter commands. Autocomplete identifies keywords and suggests matches, allowing you to streamline input by selecting from relevant options instead of typing full commands or field values. + +#### How It Works +Autocomplete operates based on the word at your current caret position: + +* As you begin typing a command or field, suggestions will appear that match the word under your current caret position. For example, typing `ad` will display a list of commands beginning with `ad`, like `add`. + * Autocomplete matches with the text characters before your caret position of the word under your current caret position. For example, when typing in the command keyword `ad` and you move your cursor between `a` and `d`, autocomplete will show you all commands starting with `a`. You can utilize this to do some cool tricks as explained in the tips section below. +* Autocomplete for command keywords e.g. `clear`, `delete`, `add` applies only to the first word you type in the command box. This initial word is treated as the command. +* Autocomplete for student fields applies to all subsequent words after the first word. All subsequent words after the first are treated as student fields with specific prefixes, e.g. `m/Math`, `g/male` + +
:notebook: **Note:** Autocomplete will **not** match subsequent words after the first word you entered with command keywords! Similarly, Autocomplete will **not** match the first word you entered with student fields!
+ +#### Supported Commands +Autocomplete supports all existing command keywords when matching. + +#### Supported Fields +Autocomplete currently supports the following fields with these prefixes: + +| Prefix | Field | Description | +|----------|------------------|-------------------------------------------------| +| `m/` | Modules | Matches **all existing** module names | +| `t/` | Tags | Matches **all existing** tags | +| `g/` | Gender | Matches gender values: `male` or `female` | +| `pa/` | File Paths | Matches **all existing** archived file paths | + +When these prefixes are detected, autocomplete automatically displays a list of suggestions related to these fields. The list of suggestions are generated through the existing list of students inside StoreClass. + +#### Example Usage +If you begin typing `edit 1 m/M`, Autocomplete will provide suggestions for available modules starting with the letter `M`, helping you to quickly select the correct module name. Similarly, typing `t/` after the command will bring up a list of tags, allowing you to specify tags accurately without needing to remember or retype exact names. + +
:notebook: **Note:** Autocomplete is **case-insensitive**!
+ +
+:question: **Common Question:** +Why are there no suggestions when I type in `m/`, `t/` or `pa/`?
+Autocomplete searches for suggestions relevant to these fields based on the existing data in StoreClass. If there are no data or students inside StoreClass, then no suggestions will be +generated for these fields. This usually occurs after a `clear` command. +
+ +
+:bulb: **Tips:** for Efficient Usage
+ +1. **Start with the command**: Autocomplete only activates for commands when typing the first word.
+2. **Remember to use prefixes**: For fields, make sure to use the correct prefix (`m/`, `t/`, `g/`, `pa/`) to activate Autocomplete for those fields.
+3. **Select from suggestions using arrow keys**: Save time by selecting from the suggestion list using arrow keys rather than typing full names or values.
+4. **Typos**: When you accidentally type in the wrong name for an existing field, instead of holding backspace and retyping the entire field, simply move the caret position over to the +prefix, and select from the list of suggestions. Autocomplete will replace the entire field with your selection for you. +
+ +By utilizing Autocomplete, you can input commands more quickly, reduce typos, and improve your overall efficiency in navigating the software! + +![Autocomplete example when keying in gender](images/AutocompleteExample.png) + ### 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. +StoreClass data are saved in the hard disk automatically after any command you enter 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. +StoreClass 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, StoreClass 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 StoreClass to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly.
-### Archiving data files `[coming in v2.0]` -_Details coming soon ..._ -------------------------------------------------------------------------------------------------------------------- ## FAQ **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous StoreClass home folder. -------------------------------------------------------------------------------------------------------------------- @@ -190,10 +454,16 @@ _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 g/GENDER p/PHONE_NUMBER m/MODULE... [t/TAG]…​`
e.g., `add n/James Ho g/male p/83216574 m/English m/Chemistry t/new t/IB` **Clear** | `clear` **Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` +**Edit** | `edit INDEX [n/NAME] [g/GENDER] [p/PHONE_NUMBER] [m/MODULE] [t/TAG]…​`
e.g.,`edit 2 n/James Lee` **Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` +**Grade** | `grade INDEX [m/MODULE s/GRADE]`
e.g., `grade 1 m/History s/85` +**Undo** | `undo` +**Redo** | `redo` **List** | `list` **Help** | `help` +**Archive** | `archive pa/PATH` +**Load** | `load pa/PATH` + diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..eb9ceb66583 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "StoreClass" theme: minima header_pages: @@ -6,9 +6,23 @@ header_pages: - DeveloperGuide.md - AboutUs.md +sidebar: + - title: "User Guide" + url: UserGuide.html + path: "UserGuide.md" + toc: true + - title: "Developer Guide" + url: DeveloperGuide.html + path: "DeveloperGuide.md" + toc: true + - title: "About Us" + url: AboutUs.html + path: "AboutUs.md" + toc: false + markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2425S1-CS2103T-W10-4/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index e092cd572e0..96995229520 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -1,18 +1,81 @@ - {%- include head.html -%} +{%- include head.html -%} - + - {%- include header.html -%} +{%- include header.html -%} -
-
- {{ content }} -
-
+ +
+ - +
+
+ {{ content }} +
+
+
+ + + diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..865aa38046d 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: "StoreClass"; font-size: 32px; } } diff --git a/docs/diagrams/ArchiveAndLoadInitialState.puml b/docs/diagrams/ArchiveAndLoadInitialState.puml new file mode 100644 index 00000000000..d0154abf2a3 --- /dev/null +++ b/docs/diagrams/ArchiveAndLoadInitialState.puml @@ -0,0 +1,19 @@ +@startuml +!include style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 +skinparam ClassBackgroundColor #FFFFAA + +title Initial State + +package data as DataPkg { + class State1 as "addressBook.json:File" +} + +package archive as ArchivePkg { + class State2 as "archiveFile1.json:File" +} + +class Pointer as "Current Working AddressBook" #FFFFFF +Pointer -up-> State1 +@end diff --git a/docs/diagrams/ArchiveSequenceDiagram-Logic.puml b/docs/diagrams/ArchiveSequenceDiagram-Logic.puml new file mode 100644 index 00000000000..f8ab96e2b98 --- /dev/null +++ b/docs/diagrams/ArchiveSequenceDiagram-Logic.puml @@ -0,0 +1,58 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant "u:ArchiveCommand" as ArchiveCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +box Storage STORAGE_COLOR_T1 +participant "StorageManager" as Storage STORAGE_COLOR +end box + +[-> LogicManager : execute(archive pa/...) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(archive pa/...) +activate AddressBookParser + +create ArchiveCommand +AddressBookParser -> ArchiveCommand +activate ArchiveCommand + +ArchiveCommand --> AddressBookParser +deactivate ArchiveCommand + +AddressBookParser --> LogicManager : u +deactivate AddressBookParser + +LogicManager -> "Storage" : saveArchivedAddressBook() +activate "Storage" + +"Storage" --> LogicManager : +deactivate "Storage" + +LogicManager -> ArchiveCommand : execute() +activate ArchiveCommand + + +ArchiveCommand -> Model : setAddressBook(new\n AddressBook()) +activate Model + +Model --> ArchiveCommand +deactivate Model + +ArchiveCommand --> LogicManager : result +deactivate ArchiveCommand +ArchiveCommand -[hidden]-> LogicManager : result +destroy ArchiveCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ArchiveToExistingFile.puml b/docs/diagrams/ArchiveToExistingFile.puml new file mode 100644 index 00000000000..faedc8675e4 --- /dev/null +++ b/docs/diagrams/ArchiveToExistingFile.puml @@ -0,0 +1,24 @@ +@startuml +!include style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 +skinparam ClassBackgroundColor #FFFFAA + +title After Command "archive pa/archiveFile1.json" + +package data as DataPkg { + class State1 as "addressBook.json:File" +} + +package archive as ArchivePkg { + class State2 as "archiveFile1.json:File" +} + + +class Pointer1 as "The data here is cleared" #FFFFFF +Pointer1 -up-> State1 + +class Pointer2 as "Containing data from addressBook.json" #FFFFFF +Pointer2 -up-> State2 +note right on link: The original data in this file \n will be overwritten. +@end diff --git a/docs/diagrams/ArchiveToNewFile.puml b/docs/diagrams/ArchiveToNewFile.puml new file mode 100644 index 00000000000..c32bb3ea491 --- /dev/null +++ b/docs/diagrams/ArchiveToNewFile.puml @@ -0,0 +1,26 @@ +@startuml +!include style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 +skinparam ClassBackgroundColor #FFFFAA + +title After Command "archive pa/archiveFile2.json" + +package data as DataPkg { + class State1 as "addressBook:File" +} + +package archive as ArchivePkg { + class State2 as "archiveFile1:AddressBook" + class State3 as "archiveFile2:AddressBook" +} + + +class Pointer1 as "Current working addressBook" #FFFFFF +Pointer1 -up-> State1 +note right on link: The original data in this file \n will be discarded. + +class Pointer2 as "Containing data of original ab0" #FFFFFF +Pointer2 -up-> State3 + +@end diff --git a/docs/diagrams/Load.puml b/docs/diagrams/Load.puml new file mode 100644 index 00000000000..b0e7f6a7d1c --- /dev/null +++ b/docs/diagrams/Load.puml @@ -0,0 +1,22 @@ +@startuml +!include style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 +skinparam ClassBackgroundColor #FFFFAA + +title After Command "archive pa/archiveFile1.json" + +package data as DataPkg { + class State1 as "addressBook.json:File" +} + +package archive as ArchivePkg { + class State2 as "archiveFile1.json:File" +} + + +class Pointer1 as "Current working addressBook\n Containing data from archiveFile1.json" #FFFFFF +Pointer1 -up-> State1 +note right on link: The original data in this file \n will be discarded. + +@end diff --git a/docs/diagrams/LoadSequenceDiagram-Logic.puml b/docs/diagrams/LoadSequenceDiagram-Logic.puml new file mode 100644 index 00000000000..4546635a5bf --- /dev/null +++ b/docs/diagrams/LoadSequenceDiagram-Logic.puml @@ -0,0 +1,71 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant "u:LoadCommand" as LoadCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +box Storage STORAGE_COLOR_T1 +participant "StorageManager" as Storage STORAGE_COLOR +end box + +[-> LogicManager : execute(load pa/...) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(load pa/...) +activate AddressBookParser + +create LoadCommand +AddressBookParser -> LoadCommand +activate LoadCommand + +LoadCommand --> AddressBookParser +deactivate LoadCommand + +AddressBookParser --> LogicManager : u +deactivate AddressBookParser + +LogicManager -> "Storage" : saveArchivedAddressBook() +activate "Storage" + +"Storage" --> LogicManager : +deactivate "Storage" + +LogicManager -> LoadCommand : execute() +activate LoadCommand + +LoadCommand --> LogicManager : result +deactivate LoadCommand +LoadCommand -[hidden]-> LogicManager : result + + +LogicManager -> LogicManager : UpdateModelWithStorage() +activate LogicManager + +LogicManager -> "Storage" : ReadAddressBook(...) +activate "Storage" + +"Storage" --> LogicManager +deactivate "Storage" + +LogicManager -> Model : SetAddressBook(...) +activate Model + +Model --> LogicManager +deactivate Model + +LogicManager --> LogicManager +deactivate LogicManager + +destroy LoadCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..599b94c521b 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -14,11 +14,11 @@ Class UserPrefs Class UniquePersonList Class Person -Class Address -Class Email Class Name Class Phone +Class Module Class Tag +Class Gender Class I #FFFFFF } @@ -39,16 +39,14 @@ AddressBook *--> "1" UniquePersonList UniquePersonList --> "~* all" Person Person *--> Name Person *--> Phone -Person *--> Email -Person *--> Address +Person *--> Module +Person *--> Gender Person *--> "*" Tag Person -[hidden]up--> I UniquePersonList -[hidden]right-> I Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email ModelManager --> "~* filtered" Person @enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..75da465df91 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -6,6 +6,15 @@ skinparam classBackgroundColor STORAGE_COLOR package Storage as StoragePackage { +package "AddressBook Storage" #F4F6F6{ +Class "<>\nAddressBookStorage" as AddressBookStorage +Class JsonAddressBookStorage +Class JsonSerializableAddressBook +Class JsonAdaptedPerson +Class JsonAdaptedTag +Class JsonAdaptedModule +} + package "UserPrefs Storage" #F4F6F6{ Class "<>\nUserPrefsStorage" as UserPrefsStorage Class JsonUserPrefsStorage @@ -14,13 +23,7 @@ Class JsonUserPrefsStorage Class "<>\nStorage" as Storage Class StorageManager -package "AddressBook Storage" #F4F6F6{ -Class "<>\nAddressBookStorage" as AddressBookStorage -Class JsonAddressBookStorage -Class JsonSerializableAddressBook -Class JsonAdaptedPerson -Class JsonAdaptedTag -} + } @@ -39,5 +42,5 @@ JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson JsonAdaptedPerson --> "*" JsonAdaptedTag - +JsonAdaptedPerson --> "*" JsonAdaptedModule @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..bc53e21f812 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -15,6 +15,9 @@ Class PersonListPanel Class PersonCard Class StatusBarFooter Class CommandBox +Class DialogBox +Class HBox +Class AutocompleteParser } package Model <> { @@ -31,6 +34,7 @@ HiddenOutside ..> Ui UiManager .left.|> Ui UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox +MainWindow *-down-> "1" DialogBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel MainWindow *-down-> "1" StatusBarFooter @@ -42,6 +46,8 @@ MainWindow -left-|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart +CommandBox --down-> "1" AutocompleteParser +DialogBox --|> HBox PersonListPanel --|> UiPart PersonCard --|> UiPart StatusBarFooter --|> UiPart diff --git a/docs/images/ArchiveAndLoadInitialState.png b/docs/images/ArchiveAndLoadInitialState.png new file mode 100644 index 00000000000..9e6b4ebe1a1 Binary files /dev/null and b/docs/images/ArchiveAndLoadInitialState.png differ diff --git a/docs/images/ArchiveSequenceDiagram-Logic.png b/docs/images/ArchiveSequenceDiagram-Logic.png new file mode 100644 index 00000000000..d325b47eb03 Binary files /dev/null and b/docs/images/ArchiveSequenceDiagram-Logic.png differ diff --git a/docs/images/ArchiveToExistingFile.png b/docs/images/ArchiveToExistingFile.png new file mode 100644 index 00000000000..11508945a78 Binary files /dev/null and b/docs/images/ArchiveToExistingFile.png differ diff --git a/docs/images/ArchiveToNewFile.png b/docs/images/ArchiveToNewFile.png new file mode 100644 index 00000000000..25a0a3fac11 Binary files /dev/null and b/docs/images/ArchiveToNewFile.png differ diff --git a/docs/images/AutocompleteExample.png b/docs/images/AutocompleteExample.png new file mode 100644 index 00000000000..24ef9c311e3 Binary files /dev/null and b/docs/images/AutocompleteExample.png differ diff --git a/docs/images/Load.png b/docs/images/Load.png new file mode 100644 index 00000000000..acf4de4e619 Binary files /dev/null and b/docs/images/Load.png differ diff --git a/docs/images/LoadSequenceDiagram-Logic.png b/docs/images/LoadSequenceDiagram-Logic.png new file mode 100644 index 00000000000..cd15379f869 Binary files /dev/null and b/docs/images/LoadSequenceDiagram-Logic.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..b4fe43c91ea 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/StoreClass-Logo.png b/docs/images/StoreClass-Logo.png new file mode 100644 index 00000000000..4c04941c824 Binary files /dev/null and b/docs/images/StoreClass-Logo.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..b28160cdb27 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..abe0f956510 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoRedoExample1.png b/docs/images/UndoRedoExample1.png new file mode 100644 index 00000000000..3af1f3dc635 Binary files /dev/null and b/docs/images/UndoRedoExample1.png differ diff --git a/docs/images/UndoRedoExample2.png b/docs/images/UndoRedoExample2.png new file mode 100644 index 00000000000..18b6311196f Binary files /dev/null and b/docs/images/UndoRedoExample2.png differ diff --git a/docs/images/UndoRedoExample3.png b/docs/images/UndoRedoExample3.png new file mode 100644 index 00000000000..757d1d2e8d9 Binary files /dev/null and b/docs/images/UndoRedoExample3.png differ diff --git a/docs/images/UndoRedoExample4.png b/docs/images/UndoRedoExample4.png new file mode 100644 index 00000000000..006dfc17335 Binary files /dev/null and b/docs/images/UndoRedoExample4.png differ diff --git a/docs/images/findResult.png b/docs/images/findResult.png new file mode 100644 index 00000000000..8c692b206a2 Binary files /dev/null and b/docs/images/findResult.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..6e75c706e43 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/isaactodo.png b/docs/images/isaactodo.png new file mode 100644 index 00000000000..4693c2c61b9 Binary files /dev/null and b/docs/images/isaactodo.png differ diff --git a/docs/images/jianwen0451.png b/docs/images/jianwen0451.png new file mode 100644 index 00000000000..f24ce6c23b7 Binary files /dev/null and b/docs/images/jianwen0451.png differ diff --git a/docs/images/prabhu.png b/docs/images/prabhu.png new file mode 100644 index 00000000000..bff5177be60 Binary files /dev/null and b/docs/images/prabhu.png differ diff --git a/docs/images/radieonajax.png b/docs/images/radieonajax.png new file mode 100644 index 00000000000..ecd7842e3e9 Binary files /dev/null and b/docs/images/radieonajax.png differ diff --git a/docs/images/storeclass.png b/docs/images/storeclass.png new file mode 100644 index 00000000000..2b326ac4fcf Binary files /dev/null and b/docs/images/storeclass.png differ diff --git a/docs/images/tanyqcollin.png b/docs/images/tanyqcollin.png new file mode 100644 index 00000000000..816565d4525 Binary files /dev/null and b/docs/images/tanyqcollin.png differ diff --git a/docs/images/yongqqqq.png b/docs/images/yongqqqq.png new file mode 100644 index 00000000000..baaf8dd98bc Binary files /dev/null and b/docs/images/yongqqqq.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..bb4b59411ce 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,18 @@ --- layout: page -title: AddressBook Level-3 +title: StoreClass --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![Java CI](https://github.com/AY2425S1-CS2103T-W10-4/tp/actions/workflows/gradle.yml/badge.svg?branch=master)](https://github.com/AY2425S1-CS2103T-W10-4/tp/actions/workflows/gradle.yml) +[![codecov](https://codecov.io/gh/AY2425S1-CS2103T-W10-4/tp/branch/master/graph/badge.svg)](https://codecov.io/gh/AY2425S1-CS2103T-W10-4/tp) ![Ui](images/Ui.png) +**StoreClass is an application designed to be used by educators in private educational institutions such as tuition +centers.** StoreClass allows educators to quickly access student information such as contact details, class details and +other necessary information. -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). - -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +* If you are interested in using StoreClass, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing StoreClass, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/docs/team/johndoe.md b/docs/team/RadieonAjax.md similarity index 100% rename from docs/team/johndoe.md rename to docs/team/RadieonAjax.md diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 678ddc8c218..4ee111ee9e4 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -36,7 +36,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(1, 3, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..c5c19d5e57b 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -6,6 +6,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.Arrays; +import java.util.regex.Pattern; /** * Helper functions for handling strings. @@ -38,6 +39,38 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { .anyMatch(preppedWord::equalsIgnoreCase); } + /** + * Returns true if the {@code phoneNumber} contains the {@code number}. + * It requires at least one number to match the phoneNumber. + *
examples:
+     *       containsNumber("123456", "123") == true
+     *       containsNumber("123456", "789") == false
+     *       containsNumber("123456", "124") == false
+     *       
+ * @param phoneNumber cannot be null, must be a single number + * @param inputNumber cannot be null, cannot be empty, must be a single number + */ + public static boolean containsNumber(String phoneNumber, String inputNumber) { + requireNonNull(phoneNumber); + requireNonNull(inputNumber); + + String preppedNumber = inputNumber.trim(); + checkArgument(!preppedNumber.isEmpty(), "Number parameter cannot be empty"); + + String preppedPhoneNumber = phoneNumber.trim(); + if (preppedPhoneNumber.isEmpty()) { + return false; + } + + //phone number should not have any space + checkArgument(preppedNumber.split("\\s+").length == 1, "Number parameter should be a single word"); + checkArgument(phoneNumber.split("\\s+").length == 1, "phoneNumber parameter should be a single word"); + + Pattern pattern = Pattern.compile("^\\d+$"); + + return pattern.matcher(phoneNumber).matches() && phoneNumber.contains(inputNumber); + } + /** * Returns a detailed message of the t, including the stack trace. */ diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..d913c5c2f1b 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -8,8 +8,11 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.logic.commands.ArchiveCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.LoadCommand; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.AddressBookParser; import seedu.address.logic.parser.exceptions.ParseException; @@ -26,6 +29,7 @@ public class LogicManager implements Logic { public static final String FILE_OPS_PERMISSION_ERROR_FORMAT = "Could not save data to file %s due to insufficient permissions to write to the file or the folder."; + public static final String LOAD_ERROR_FORMAT = "Could not load date due to the following error: %s"; private final Logger logger = LogsCenter.getLogger(LogicManager.class); @@ -46,11 +50,15 @@ public LogicManager(Model model, Storage storage) { public CommandResult execute(String commandText) throws CommandException, ParseException { logger.info("----------------[USER COMMAND][" + commandText + "]"); + ReadOnlyAddressBook original = model.getAddressBook(); CommandResult commandResult; Command command = addressBookParser.parseCommand(commandText); + archiveIfNeeded(command, storage, model); + commandResult = command.execute(model); try { + updateModelWithStorage(command, storage, model); storage.saveAddressBook(model.getAddressBook()); } catch (AccessDeniedException e) { throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e); @@ -85,4 +93,31 @@ public GuiSettings getGuiSettings() { public void setGuiSettings(GuiSettings guiSettings) { model.setGuiSettings(guiSettings); } + + private void updateModelWithStorage(Command command, Storage storage, Model model) throws CommandException { + try { + if (command instanceof LoadCommand) { + ReadOnlyAddressBook readOnlyAddressBook = storage.readAddressBook(((LoadCommand) command) + .getLoadPath()).get(); + model.setAddressBook(readOnlyAddressBook); + model.updateAddressBook(readOnlyAddressBook); + } + } catch (DataLoadingException e) { + throw new CommandException(String.format(LOAD_ERROR_FORMAT, e.getMessage()), e); + } + + } + + private void archiveIfNeeded(Command command, Storage storage, Model model) throws CommandException { + try { + ReadOnlyAddressBook original = model.getAddressBook(); + if (command instanceof ArchiveCommand) { + storage.saveArchivedAddressBook(original, ((ArchiveCommand) command).getArchivePath()); + } + } catch (AccessDeniedException e) { + throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e); + } catch (IOException ioe) { + throw new CommandException(String.format(FILE_OPS_ERROR_FORMAT, ioe.getMessage()), ioe); + } + } } diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..9a984c9e883 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -39,11 +39,9 @@ public static String format(Person person) { builder.append(person.getName()) .append("; Phone: ") .append(person.getPhone()) - .append("; Email: ") - .append(person.getEmail()) - .append("; Address: ") - .append(person.getAddress()) - .append("; Tags: "); + .append("; Module: "); + person.getModules().forEach(builder::append); + builder.append("; Tags: "); person.getTags().forEach(builder::append); return builder.toString(); } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..60c895bb2a4 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -1,8 +1,8 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GENDER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; 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; @@ -24,14 +24,15 @@ public class AddCommand extends Command { + "Parameters: " + PREFIX_NAME + "NAME " + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_GENDER + "GENDER " + + PREFIX_MODULE + "MODULE... " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " + + PREFIX_GENDER + "male " + + PREFIX_MODULE + "MA2103 " + + PREFIX_MODULE + "CS2103T " + PREFIX_TAG + "friends " + PREFIX_TAG + "owesMoney"; @@ -57,6 +58,7 @@ public CommandResult execute(Model model) throws CommandException { } model.addPerson(toAdd); + model.saveAddressBook(); return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); } diff --git a/src/main/java/seedu/address/logic/commands/ArchiveCommand.java b/src/main/java/seedu/address/logic/commands/ArchiveCommand.java new file mode 100644 index 00000000000..9c43ec45109 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ArchiveCommand.java @@ -0,0 +1,55 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PATH; + +import java.nio.file.Path; + +import seedu.address.model.Model; +/** + * Archives the address book. + */ +public class ArchiveCommand extends Command { + + public static final String COMMAND_WORD = "archive"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " " + PREFIX_PATH + "FileName\n" + + "The file name should be a path of an json file " + + "and not contain any slash `/` \n" + + "There should be only one path provided.\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_PATH + "mybook.json"; + public static final String MESSAGE_SUCCESS = "Archive the file to: %1$s"; + + private Path archivePath; + + public ArchiveCommand(Path archivePath) { + this.archivePath = archivePath; + } + + public Path getArchivePath() { + return archivePath; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.clearAddressBook(); + return new CommandResult(String.format(MESSAGE_SUCCESS, archivePath)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ArchiveCommand)) { + return false; + } + + ArchiveCommand otherLoadCommand = (ArchiveCommand) other; + return archivePath.equals(otherLoadCommand.archivePath); + } + +} + diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..a234ec93b02 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -18,6 +18,7 @@ public class ClearCommand extends Command { public CommandResult execute(Model model) { requireNonNull(model); model.setAddressBook(new AddressBook()); + model.saveAddressBook(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..0b0658c151a 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -42,6 +42,7 @@ public CommandResult execute(Model model) throws CommandException { Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); model.deletePerson(personToDelete); + model.saveAddressBook(); return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..ae63826fbc0 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -1,8 +1,8 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GENDER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; 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; @@ -21,8 +21,8 @@ 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.Gender; +import seedu.address.model.person.Module; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -41,12 +41,11 @@ public class EditCommand extends Command { + "Parameters: INDEX (must be a positive integer) " + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_GENDER + "GENDER] " + + "[" + PREFIX_MODULE + "MODULE] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; + + PREFIX_PHONE + "91234567 "; public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; @@ -85,6 +84,7 @@ public CommandResult execute(Model model) throws CommandException { model.setPerson(personToEdit, editedPerson); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.saveAddressBook(); return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); } @@ -97,11 +97,12 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + Gender updatedGender = editPersonDescriptor.getGender().orElse(personToEdit.getGender()); + Set updatedModules = editPersonDescriptor.getModules().orElse(personToEdit.getModules()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(updatedName, updatedPhone, updatedGender, + updatedModules, updatedTags); } @Override @@ -135,8 +136,8 @@ public String toString() { public static class EditPersonDescriptor { private Name name; private Phone phone; - private Email email; - private Address address; + private Gender gender; + private Set modules; private Set tags; public EditPersonDescriptor() {} @@ -148,8 +149,8 @@ public EditPersonDescriptor() {} public EditPersonDescriptor(EditPersonDescriptor toCopy) { setName(toCopy.name); setPhone(toCopy.phone); - setEmail(toCopy.email); - setAddress(toCopy.address); + setGender(toCopy.gender); + setModules(toCopy.modules); setTags(toCopy.tags); } @@ -157,7 +158,7 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, gender, modules, tags); } public void setName(Name name) { @@ -176,20 +177,20 @@ public Optional getPhone() { return Optional.ofNullable(phone); } - public void setEmail(Email email) { - this.email = email; + public void setModules(Set modules) { + this.modules = (modules != null) ? new HashSet<>(modules) : null; } - public Optional getEmail() { - return Optional.ofNullable(email); + public Optional> getModules() { + return (modules != null) ? Optional.of(Collections.unmodifiableSet(modules)) : Optional.empty(); } - public void setAddress(Address address) { - this.address = address; + public void setGender(Gender gender) { + this.gender = gender; } - public Optional
getAddress() { - return Optional.ofNullable(address); + public Optional getGender() { + return Optional.ofNullable(gender); } /** @@ -223,8 +224,8 @@ public boolean equals(Object other) { 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(gender, otherEditPersonDescriptor.gender) + && Objects.equals(modules, otherEditPersonDescriptor.modules) && Objects.equals(tags, otherEditPersonDescriptor.tags); } @@ -233,8 +234,8 @@ public String toString() { return new ToStringBuilder(this) .add("name", name) .add("phone", phone) - .add("email", email) - .add("address", address) + .add("gender", gender) + .add("modules", modules) .add("tags", tags) .toString(); } diff --git a/src/main/java/seedu/address/logic/commands/FilterCommand.java b/src/main/java/seedu/address/logic/commands/FilterCommand.java new file mode 100644 index 00000000000..e2a3f884d78 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FilterCommand.java @@ -0,0 +1,190 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.FilterPredicate; +import seedu.address.model.person.Gender; +import seedu.address.model.person.Module; +import seedu.address.model.person.Name; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +/** + * Filters and list out all person in address book whose details match multiple specified conditions. + * Matching process is case-insensitive and utilizes the provided keywords to filer result. + */ +public class FilterCommand extends Command { + + public static final String COMMAND_WORD = "filter"; + public static final String MESSAGE_NOT_FILTERED = "At least one field to filter must be provided."; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filter all persons whose details contain all " + + "the specified keywords (case-insensitive) as condition and display them as a list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " n/joe m/MA1521"; + + private final FilterPersonDescriptor filterPersonDescriptor; + private final FilterPredicate predicate; + + /** + * @param filterPersonDescriptor details to filter thr person with + */ + public FilterCommand(FilterPersonDescriptor filterPersonDescriptor) { + requireNonNull(filterPersonDescriptor); + + this.filterPersonDescriptor = filterPersonDescriptor; + this.predicate = new FilterPredicate(filterPersonDescriptor); + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FilterCommand)) { + return false; + } + + FilterCommand otherFilterCommand = (FilterCommand) other; + return filterPersonDescriptor.equals(otherFilterCommand.filterPersonDescriptor); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("filterPersonDescriptor", filterPersonDescriptor) + .toString(); + } + + /** + * Stores the details to filter the person with. Each non-empty field value will replace the + * corresponding field value of the person. + */ + public static class FilterPersonDescriptor { + private Name name; + private Phone phone; + private Gender gender; + private Set modules; + private Set tags; + + public FilterPersonDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public FilterPersonDescriptor(FilterPersonDescriptor toCopy) { + setName(toCopy.name); + setPhone(toCopy.phone); + setGender(toCopy.gender); + setModules(toCopy.modules); + setTags(toCopy.tags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldFiltered() { + return CollectionUtil.isAnyNonNull(name, phone, gender, modules, tags); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setPhone(Phone phone) { + this.phone = phone; + } + + public Optional getPhone() { + return Optional.ofNullable(phone); + } + + public void setModules(Set modules) { + this.modules = (modules != null) ? new HashSet<>(modules) : null; + } + + public Optional> getModules() { + return (modules != null) ? Optional.of(Collections.unmodifiableSet(modules)) : Optional.empty(); + } + + public void setGender(Gender gender) { + this.gender = gender; + } + + public Optional getGender() { + return Optional.ofNullable(gender); + } + + /** + * Sets {@code tags} to this object's {@code tags}. + * A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + + /** + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Optional> getTags() { + return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FilterPersonDescriptor)) { + return false; + } + + FilterPersonDescriptor otherFilterPersonDescriptor = (FilterPersonDescriptor) other; + return Objects.equals(name, otherFilterPersonDescriptor.name) + && Objects.equals(phone, otherFilterPersonDescriptor.phone) + && Objects.equals(gender, otherFilterPersonDescriptor.gender) + && Objects.equals(modules, otherFilterPersonDescriptor.modules) + && Objects.equals(tags, otherFilterPersonDescriptor.tags); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", name) + .add("phone", phone) + .add("gender", gender) + .add("modules", modules) + .add("tags", tags) + .toString(); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..a6db289701f 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -5,24 +5,24 @@ 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.person.DetailContainsKeywordsPredicate; /** - * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Finds and lists all persons in address book whose details contains any of the argument keywords. * Keyword matching is case insensitive. */ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose name/tag contain any of " + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + "Example: " + COMMAND_WORD + " alice bob charlie"; - private final NameContainsKeywordsPredicate predicate; + private final DetailContainsKeywordsPredicate predicate; - public FindCommand(NameContainsKeywordsPredicate predicate) { + public FindCommand(DetailContainsKeywordsPredicate predicate) { this.predicate = predicate; } diff --git a/src/main/java/seedu/address/logic/commands/GradeCommand.java b/src/main/java/seedu/address/logic/commands/GradeCommand.java new file mode 100644 index 00000000000..27ab11761f3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/GradeCommand.java @@ -0,0 +1,114 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GRADE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Module; +import seedu.address.model.person.Person; + +/** + * Grades a student in the address book. + */ +public class GradeCommand extends Command { + public static final String COMMAND_WORD = "grade"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Grades a student identified by the index number " + + "used in the displayed person list with a numerical score.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_MODULE + "MODULE " + + PREFIX_GRADE + "GRADE\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_MODULE + "MA2103 " + + PREFIX_GRADE + "80"; + public static final String MESSAGE_GRADE_SUCCESS = "Graded Module: %1$s for %2$s"; + public static final String MESSAGE_INVALID_GRADE = "The grade provided is invalid."; + public static final String MESSAGE_INVALID_MODULE = "The module specified is not found."; + public static final String MESSAGE_INVALID_PERSON = "The person index provided is invalid."; + public static final String MESSAGE_MISMATCH_MODULE_GRADE = "The number of modules and grades" + + "provided do not match."; + + private final Index targetIndex; + private final Map moduleGrades; + + /** + * Creates a GradeCommand to grade the specified {@code Person} + */ + public GradeCommand(Index targetIndex, Map moduleGrades) { + requireNonNull(targetIndex, "Target index cannot be null"); + requireNonNull(moduleGrades, "Module cannot be null"); + this.targetIndex = targetIndex; + this.moduleGrades = new LinkedHashMap<>(moduleGrades); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + Person personToGrade = lastShownList.get(targetIndex.getZeroBased()); + Set personModules = new HashSet<>(); + for (Module mod : personToGrade.getModules()) { + // Make a copy of each Module to avoid modifying the original + personModules.add(new Module(mod.getModule(), mod.getGrade())); + } + + List gradeResults = new ArrayList<>(); + + for (Map.Entry entry : moduleGrades.entrySet()) { + String moduleName = entry.getKey(); + Integer grade = entry.getValue(); + + if (grade < 0 || grade > 100) { + throw new CommandException(MESSAGE_INVALID_GRADE); + } + + Optional moduleToGrade = personModules.stream() + .filter(m -> m.module.equals(moduleName)) + .findFirst(); + + if (moduleToGrade.isEmpty()) { + throw new CommandException(MESSAGE_INVALID_MODULE + " (" + moduleName + ")"); + } + + // Replace the module with a new instance containing the grade + Module updatedModule = new Module(moduleName, grade.toString()); + personModules.remove(moduleToGrade.get()); + personModules.add(updatedModule); + + gradeResults.add(String.format("%s: %d", moduleName, grade)); + } + + Person updatedPerson = new Person(personToGrade.getName(), personToGrade.getPhone(), + personToGrade.getGender(), personModules, personToGrade.getTags()); + model.setPerson(personToGrade, updatedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.saveAddressBook(); + + String gradeResultsString = String.join("\n", gradeResults); + return new CommandResult(String.format(MESSAGE_GRADE_SUCCESS, personToGrade.getName(), gradeResultsString)); + } + + @Override + public boolean equals(Object other) { + return other == this || (other instanceof GradeCommand + && targetIndex.equals(((GradeCommand) other).targetIndex) + && moduleGrades.equals(((GradeCommand) other).moduleGrades)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/LoadCommand.java b/src/main/java/seedu/address/logic/commands/LoadCommand.java new file mode 100644 index 00000000000..cb28b401c7b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LoadCommand.java @@ -0,0 +1,53 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_PATH; + +import java.nio.file.Path; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * This command is to load the model with a file at the path + * */ +public class LoadCommand extends Command { + + public static final String COMMAND_WORD = "load"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " " + PREFIX_PATH + "FileName\n" + + "The file name should be a path of an existing json file containing an address book " + + "and not contain any slash `/` \n" + + "There should be only one path provided.\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_PATH + "mybook.json"; + public static final String MESSAGE_SUCCESS = "Loading file from: %1$s"; + + private Path loadPath; + + public LoadCommand(Path loadPath) { + this.loadPath = loadPath; + } + + public Path getLoadPath() { + return loadPath; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + model.clearAddressBook(); + return new CommandResult(String.format(MESSAGE_SUCCESS, loadPath)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof LoadCommand)) { + return false; + } + + LoadCommand otherLoadCommand = (LoadCommand) other; + return loadPath.equals(otherLoadCommand.loadPath); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java new file mode 100644 index 00000000000..7ddf12378d7 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -0,0 +1,39 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Represents a command that redoes the last undone action in the address book. + */ +public class RedoCommand extends Command { + public static final String COMMAND_WORD = "redo"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Reapplies the last undone change to the address book.\n" + + "Example: " + COMMAND_WORD; + public static final String MESSAGE_REDO_SUCCESS = "Successfully redone the last undone action!"; + public static final String MESSAGE_REDO_FAILURE = "There are no actions to redo."; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (!model.canRedoAddressBook()) { + throw new CommandException(MESSAGE_REDO_FAILURE); + } + + redoLastAction(model); + return new CommandResult(MESSAGE_REDO_SUCCESS); + } + + /** + * Redoes the last action in the address book model. + * + * @param model The model of the application. + */ + private void redoLastAction(Model model) { + model.redoAddressBook(); // Calls the model to redo the action + } +} diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java new file mode 100644 index 00000000000..d2d847da0df --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -0,0 +1,46 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Represents a command that undoes the last action in the address book. + */ +public class UndoCommand extends Command { + public static final String COMMAND_WORD = "undo"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Reverts the address book to its previous state.\n" + + "Example: " + COMMAND_WORD; + public static final String MESSAGE_UNDO_SUCCESS = "Successfully undone the last action!"; + public static final String MESSAGE_UNDO_FAILURE = "There are no actions to undo."; + + /** + * Executes the undo command to revert the last action. + * + * @param model The model of the application, which contains the address book. + * @return The result of executing the undo command. + * @throws CommandException If there are no actions to undo. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (!model.canUndoAddressBook()) { + throw new CommandException(MESSAGE_UNDO_FAILURE); + } + + undoLastAction(model); + return new CommandResult(MESSAGE_UNDO_SUCCESS); + } + + /** + * Undoes the last action in the address book model. + * + * @param model The model of the application. + */ + private void undoLastAction(Model model) { + model.undoAddressBook(); // Calls the model to undo the action + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..6ff8f53ac61 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -1,8 +1,8 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GENDER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; 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; @@ -12,8 +12,8 @@ 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.Gender; +import seedu.address.model.person.Module; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -31,21 +31,23 @@ public class AddCommandParser implements Parser { */ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_MODULE, + PREFIX_GENDER, PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_GENDER, + PREFIX_PHONE) || !argMultimap.getPreamble().isEmpty() + || argMultimap.getAllValues(PREFIX_MODULE).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_GENDER); 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()); + Gender gender = ParserUtil.parseGender(argMultimap.getValue(PREFIX_GENDER).get()); + Set moduleList = ParserUtil.parseModules(argMultimap.getAllValues(PREFIX_MODULE)); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, gender, moduleList, tagList); return new AddCommand(person); } diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..2cf079988ff 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -9,14 +9,20 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.ArchiveCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FilterCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.GradeCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.LoadCommand; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.UndoCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -37,6 +43,7 @@ public class AddressBookParser { * @return the command based on the user input * @throws ParseException if the user input does not conform the expected format */ + @SuppressWarnings("checkstyle:Regexp") public Command parseCommand(String userInput) throws ParseException { final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); if (!matcher.matches()) { @@ -56,6 +63,9 @@ public Command parseCommand(String userInput) throws ParseException { case AddCommand.COMMAND_WORD: return new AddCommandParser().parse(arguments); + case ArchiveCommand.COMMAND_WORD: + return new ArchiveCommandParser().parse(arguments); + case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); @@ -71,12 +81,27 @@ public Command parseCommand(String userInput) throws ParseException { case ListCommand.COMMAND_WORD: return new ListCommand(); + case LoadCommand.COMMAND_WORD: + return new LoadCommandParser().parse(arguments); + + case UndoCommand.COMMAND_WORD: + return new UndoCommand(); + + case RedoCommand.COMMAND_WORD: + return new RedoCommand(); + case ExitCommand.COMMAND_WORD: return new ExitCommand(); + case FilterCommand.COMMAND_WORD: + return new FilterCommandParser().parse(arguments); + case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case GradeCommand.COMMAND_WORD: + return new GradeCommandParser().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/ArchiveCommandParser.java b/src/main/java/seedu/address/logic/parser/ArchiveCommandParser.java new file mode 100644 index 00000000000..9bba3b78e3e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ArchiveCommandParser.java @@ -0,0 +1,42 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PATH; + +import java.nio.file.Path; +import java.util.stream.Stream; + +import seedu.address.logic.commands.ArchiveCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ArchiveCommand object + */ +public class ArchiveCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ArchiveCommand + * and returns an ArchiveCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ArchiveCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_PATH); + + if (!arePrefixesPresent(argMultimap, PREFIX_PATH) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ArchiveCommand.MESSAGE_USAGE)); + } + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PATH); + Path path = ParserUtil.parsePathWithoutCheck(argMultimap.getValue(PREFIX_PATH).get()); + return new ArchiveCommand(path); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/AutocompleteParser.java b/src/main/java/seedu/address/logic/parser/AutocompleteParser.java new file mode 100644 index 00000000000..b6dedce0fff --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AutocompleteParser.java @@ -0,0 +1,380 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_GENDER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PATH; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.io.File; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; + +import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.ArchiveCommand; +import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FilterCommand; +import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.GradeCommand; +import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.LoadCommand; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.UndoCommand; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.person.Module; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Parses user input for autocomplete features. + */ +public class AutocompleteParser { + /** + * List of commands to match for when autocompleting. + */ + private static final String[] commands = { + AddCommand.COMMAND_WORD, + ArchiveCommand.COMMAND_WORD, + EditCommand.COMMAND_WORD, + DeleteCommand.COMMAND_WORD, + ClearCommand.COMMAND_WORD, + FindCommand.COMMAND_WORD, + ListCommand.COMMAND_WORD, + ExitCommand.COMMAND_WORD, + HelpCommand.COMMAND_WORD, + GradeCommand.COMMAND_WORD, + UndoCommand.COMMAND_WORD, + RedoCommand.COMMAND_WORD, + LoadCommand.COMMAND_WORD, + FilterCommand.COMMAND_WORD, + }; + + /** + * List of prefixes to match for when autocompleting. + */ + private static final Prefix[] prefixes = { + PREFIX_MODULE, + PREFIX_TAG, + PREFIX_GENDER, + PREFIX_PATH + }; + + /** + * List of genders to match for when autocompleting. + */ + private static final String[] validGenders = { + "male", + "female" + }; + + private String filePath = "archived"; + + /** + * Overloaded constructor for AutocompleteParser. Only used for testing purposes. + * + * @param filePath File directory to use when matching for file paths. + */ + public AutocompleteParser(String filePath) { + if (!filePath.isEmpty()) { + this.filePath = filePath; + } + } + + /** + * Constructor for AutocompleteParser. + */ + public AutocompleteParser() { + } + + /** + * Parses user input for a list of suggestions for autocompletion. + * + * @param userInput Full input from user. + * @param ab AddressBook to base the suggestions off of. + * @param caretPosition Current caret position in the text field. + * @return HashMap of items consisting of (item, full input from user with word under caret substituted with item). + */ + public HashMap parseCommand(String userInput, ReadOnlyAddressBook ab, int caretPosition) { + String wordUnderCaret = getWordFromCaretPosition(userInput, caretPosition); + + if (shouldReturnEmptyList(wordUnderCaret)) { + return new HashMap<>(); + } + assert(!wordUnderCaret.isEmpty()); + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(" " + wordUnderCaret.trim(), prefixes); + int startIndex = getPreviousWhitespaceIndex(userInput, caretPosition); + int endIndex = getNextWhitespaceIndex(userInput, caretPosition); + + if (isCaretOnFirstWord(userInput, caretPosition)) { + return getCommandSuggestions(userInput, wordUnderCaret, startIndex, endIndex); + } + + if (argMultimap.getValue(PREFIX_MODULE).isPresent()) { + return getModuleSuggestions(userInput, wordUnderCaret, ab, startIndex, endIndex, argMultimap); + } + + if (argMultimap.getValue(PREFIX_TAG).isPresent()) { + return getTagSuggestions(userInput, wordUnderCaret, ab, startIndex, endIndex, argMultimap); + } + + if (argMultimap.getValue(PREFIX_GENDER).isPresent()) { + return getGenderSuggestions(userInput, wordUnderCaret, startIndex, endIndex, argMultimap); + } + + if (argMultimap.getValue(PREFIX_PATH).isPresent()) { + return getPathSuggestions(userInput, wordUnderCaret, startIndex, endIndex, argMultimap); + } + + return new HashMap<>(); + } + + /** + * Checks if the conditions for returning an empty list of suggestions are met. + * + * @param wordUnderCaret The word that is currently being typed under the caret. + * @return true if word under caret is empty. + */ + private boolean shouldReturnEmptyList(String wordUnderCaret) { + return wordUnderCaret.isEmpty(); + } + + /** + * Generates suggestions for modules based on the current user input. + * + * @param userInput The full user input string. + * @param wordUnderCaret The word that is currently being typed under the caret. + * @param ab The AddressBook that contains persons with modules to search for suggestions. + * @param startIndex The index of the start of the word under the caret. + * @param endIndex The index of the end of the word under the caret. + * @param argMultimap The tokenized argument multimap containing the parsed user input. + * @return A HashMap of module suggestions. + */ + private HashMap getModuleSuggestions(String userInput, String wordUnderCaret, + ReadOnlyAddressBook ab, int startIndex, int endIndex, + ArgumentMultimap argMultimap) { + HashMap suggestionList = new HashMap<>(); + for (Person person : ab.getPersonList()) { + for (Module module : person.getModules()) { + String moduleString = module.module; + if (isStringMatching(moduleString, argMultimap.getValue(PREFIX_MODULE).get()) + && !suggestionList.containsKey(moduleString)) { + suggestionList.put(moduleString, getCompleteStringWithReplacement(userInput, wordUnderCaret, + moduleString, startIndex, endIndex)); + } + } + } + return suggestionList; + } + + /** + * Generates suggestions for tags based on the current user input. + * + * @param userInput The full user input string. + * @param wordUnderCaret The word that is currently being typed under the caret. + * @param ab The AddressBook that contains persons with tags to search for suggestions. + * @param startIndex The index of the start of the word under the caret. + * @param endIndex The index of the end of the word under the caret. + * @param argMultimap The tokenized argument multimap containing the parsed user input. + * @return A HashMap of tag suggestions. + */ + private HashMap getTagSuggestions(String userInput, String wordUnderCaret, ReadOnlyAddressBook ab, + int startIndex, int endIndex, ArgumentMultimap argMultimap) { + HashMap suggestionList = new HashMap<>(); + for (Person person : ab.getPersonList()) { + for (Tag tag : person.getTags()) { + if (isStringMatching(tag.tagName, argMultimap.getValue(PREFIX_TAG).get()) + && !suggestionList.containsKey(tag.tagName)) { + suggestionList.put(tag.tagName, getCompleteStringWithReplacement(userInput, wordUnderCaret, + tag.tagName, startIndex, endIndex)); + } + } + } + return suggestionList; + } + + /** + * Generates suggestions for genders based on the current user input. + * + * @param userInput The full user input string. + * @param wordUnderCaret The word that is currently being typed under the caret. + * @param startIndex The index of the start of the word under the caret. + * @param endIndex The index of the end of the word under the caret. + * @param argMultimap The tokenized argument multimap containing the parsed user input. + * @return A HashMap of gender suggestions. + */ + private HashMap getGenderSuggestions(String userInput, String wordUnderCaret, + int startIndex, int endIndex, ArgumentMultimap argMultimap) { + HashMap suggestionList = new HashMap<>(); + for (String gender : validGenders) { + if (isStringMatching(gender, argMultimap.getValue(PREFIX_GENDER).get())) { + suggestionList.put(gender, getCompleteStringWithReplacement(userInput, wordUnderCaret, gender, + startIndex, endIndex)); + } + } + return suggestionList; + } + + /** + * Generates suggestions for file paths based on the current user input. + * + * @param userInput The full user input string. + * @param wordUnderCaret The word that is currently being typed under the caret. + * @param startIndex The index of the start of the word under the caret. + * @param endIndex The index of the end of the word under the caret. + * @param argMultimap The tokenized argument multimap containing the parsed user input. + * @return A HashMap of file path suggestions. + */ + private HashMap getPathSuggestions(String userInput, String wordUnderCaret, + int startIndex, int endIndex, ArgumentMultimap argMultimap) { + HashMap suggestionList = new HashMap<>(); + for (String file : getAllFilesInArchiveDirectory()) { + if (isStringMatching(file, argMultimap.getValue(PREFIX_PATH).get())) { + suggestionList.put(file, getCompleteStringWithReplacement(userInput, wordUnderCaret, file, + startIndex, endIndex)); + } + } + return suggestionList; + } + + /** + * Generates suggestions for commands based on the current user input. + * + * @param userInput The full user input string. + * @param wordUnderCaret The word that is currently being typed under the caret. + * @param startIndex The index of the start of the word under the caret. + * @param endIndex The index of the end of the word under the caret. + * @return A HashMap of command suggestions. + */ + private HashMap getCommandSuggestions(String userInput, String wordUnderCaret, + int startIndex, int endIndex) { + HashMap suggestionList = new HashMap<>(); + for (String command : commands) { + // If full command has already been typed out, do not show suggestions. + if (command.equals(wordUnderCaret)) { + return new HashMap<>(); + } + if (isStringMatching(command, wordUnderCaret)) { + suggestionList.put(command, getCompleteStringWithReplacement(userInput, wordUnderCaret, command, + startIndex, endIndex)); + } + } + return suggestionList; + } + + private String getWordFromCaretPosition(String fullInput, int caretPosition) { + String text = fullInput.substring(0, caretPosition); + int startIndex = getPreviousWhitespaceIndex(text, caretPosition); + + return text.substring(startIndex + 1); + } + + private boolean isCaretOnFirstWord(String fullInput, int caretPosition) { + int startIndex = getPreviousWhitespaceIndex(fullInput, 0); + int endIndex = getNextWhitespaceIndex(fullInput, 0); + + return caretPosition >= startIndex && caretPosition <= endIndex; + } + + private int getPreviousWhitespaceIndex(String text, int caretPosition) { + int index = caretPosition - 1; + while (index >= 0 && !Character.isWhitespace(text.charAt(index))) { + index--; + } + + return index; + } + + private int getNextWhitespaceIndex(String text, int caretPosition) { + int endIndex = caretPosition; + while (endIndex < text.length() + && !Character.isWhitespace(text.charAt(endIndex))) { + endIndex++; + } + + return endIndex; + } + + /** + * Gets the full user input string with the suggestion slotted in + * + * @param fullUserInput The full user input string. + * @param word The word that is currently being typed under the caret. + * @param command The word to replace the word under the caret. + * @param startIndex The index of the start of the word under the caret. + * @param endIndex The index of the end of the word under the caret. + * @return Full user input string with the suggestion slotted in + */ + private String getCompleteStringWithReplacement(String fullUserInput, String word, String command, + int startIndex, int endIndex) { + String stringToSlotIn; + stringToSlotIn = getStringWithPrefix(word, command); + + StringBuffer buf = new StringBuffer(fullUserInput); + buf.replace(startIndex + 1, endIndex, stringToSlotIn); + return buf.toString(); + } + + /** + * Gets all the file names in the directory "archived". + * Will return an empty list if the directory does not exist. + * @return An ArrayList with all the name of json file + */ + + private ArrayList getAllFilesInArchiveDirectory() { + File directory = Paths.get(filePath).toFile(); + + // Check if the directory exists and is indeed a directory + if (directory.exists() && directory.isDirectory()) { + // Get all files and directories in the specified path + return getAllJsonFiles(directory); + } else { + // Return an empty list if the directory does not exist + return new ArrayList<>(); + } + } + + /** + * Gets all the json file name from a directory of given Path + * @param directory The file path to the directory + * @return An array list of all json file name + * */ + private ArrayList getAllJsonFiles(File directory) { + File[] files = directory.listFiles(); + assert (files != null); + ArrayList result = new ArrayList<>(); + for (File file : files) { + // add file name if it is a json file + if (isJson(file)) { + result.add(file.getName()); + } + } + return result; + } + + /** + * Checks if the given file is a json file. + * @param file the file to check + * */ + private boolean isJson(File file) { + return file.isFile() && file.getName().toLowerCase().endsWith(".json"); + } + + private String getStringWithPrefix(String word, String command) { + for (Prefix prefix : prefixes) { + if (word.startsWith(prefix.getPrefix())) { + return prefix.getPrefix() + command; + } + } + return command; + } + + private boolean isStringMatching(String targetString, String input) { + return targetString.toLowerCase().startsWith(input.toLowerCase()); + } +} + diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..7ce9c3d75f3 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -8,8 +8,10 @@ public class CliSyntax { /* Prefix definitions */ public static final Prefix PREFIX_NAME = new Prefix("n/"); public static final Prefix PREFIX_PHONE = new Prefix("p/"); - public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_GENDER = new Prefix("g/"); + public static final Prefix PREFIX_MODULE = new Prefix("m/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_GRADE = new Prefix("s/"); + public static final Prefix PREFIX_PATH = new Prefix("pa/"); } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..6ec9ac2dd23 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -2,8 +2,8 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GENDER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -17,6 +17,7 @@ import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Module; import seedu.address.model.tag.Tag; /** @@ -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_GENDER, + PREFIX_MODULE, PREFIX_TAG); Index index; @@ -42,7 +44,7 @@ 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_GENDER); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -52,12 +54,10 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); - } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + if (argMultimap.getValue(PREFIX_GENDER).isPresent()) { + editPersonDescriptor.setGender(ParserUtil.parseGender(argMultimap.getValue(PREFIX_GENDER).get())); } + parseModulesForEdit(argMultimap.getAllValues(PREFIX_MODULE)).ifPresent(editPersonDescriptor::setModules); parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { @@ -82,4 +82,19 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars return Optional.of(ParserUtil.parseTags(tagSet)); } + /** + * Parses {@code Collection modules} into a {@code Set} if {@code modules} is non-empty. + * If {@code modules} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseModulesForEdit(Collection modules) throws ParseException { + assert modules != null; + + if (modules.isEmpty()) { + return Optional.empty(); + } + Collection moduleSet = modules.size() == 1 && modules.contains("") ? Collections.emptySet() : modules; + return Optional.of(ParserUtil.parseModules(moduleSet)); + } + } diff --git a/src/main/java/seedu/address/logic/parser/FilterCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java new file mode 100644 index 00000000000..f64250dfd52 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java @@ -0,0 +1,114 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GENDER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.FilterCommand; +import seedu.address.logic.commands.FilterCommand.FilterPersonDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Module; +import seedu.address.model.person.Name; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new FilterCommandParser + */ +public class FilterCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of FilterCommand + * and return a FilterCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FilterCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_GENDER, + PREFIX_MODULE, PREFIX_TAG); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_GENDER, PREFIX_PHONE, PREFIX_MODULE, PREFIX_TAG)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_GENDER); + + FilterPersonDescriptor filterPersonDescriptor = new FilterPersonDescriptor(); + + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + filterPersonDescriptor.setName(isNameValid(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()))); + } + if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { + filterPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); + } + if (argMultimap.getValue(PREFIX_GENDER).isPresent()) { + filterPersonDescriptor.setGender(ParserUtil.parseGender(argMultimap.getValue(PREFIX_GENDER).get())); + } + parseModulesForFilter(argMultimap.getAllValues(PREFIX_MODULE)).ifPresent(filterPersonDescriptor::setModules); + parseTagsForFilter(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(filterPersonDescriptor::setTags); + + if (!filterPersonDescriptor.isAnyFieldFiltered()) { + throw new ParseException(FilterCommand.MESSAGE_NOT_FILTERED); + } + return new FilterCommand(filterPersonDescriptor); + } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseTagsForFilter(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } + + /** + * Parses {@code Collection modules} into a {@code Set} if {@code modules} is non-empty. + * If {@code modules} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseModulesForFilter(Collection modules) throws ParseException { + assert modules != null; + + if (modules.isEmpty()) { + return Optional.empty(); + } + Collection moduleSet = modules.size() == 1 && modules.contains("") ? Collections.emptySet() : modules; + return Optional.of(ParserUtil.parseModules(moduleSet)); + } + + /** + * Returns true if contains a valid prefix. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).anyMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + /** + * Returns name if the name contains one word. + * @throws ParseException If the name contains more than one word. + */ + private Name isNameValid(Name name) throws ParseException { + String[] words = name.fullName.split("\\s+"); + if (words.length != 1) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + return name; + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 2867bde857b..676e691b5a3 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -6,7 +6,7 @@ import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.DetailContainsKeywordsPredicate; /** * Parses input arguments and creates a new FindCommand object @@ -25,9 +25,9 @@ public FindCommand parse(String args) throws ParseException { String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); } - String[] nameKeywords = trimmedArgs.split("\\s+"); + String[] keywords = trimmedArgs.split("\\s+"); - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + return new FindCommand(new DetailContainsKeywordsPredicate(Arrays.asList(keywords))); } } diff --git a/src/main/java/seedu/address/logic/parser/GradeCommandParser.java b/src/main/java/seedu/address/logic/parser/GradeCommandParser.java new file mode 100644 index 00000000000..5dae0ebead6 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/GradeCommandParser.java @@ -0,0 +1,58 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.GradeCommand.MESSAGE_MISMATCH_MODULE_GRADE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GRADE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.GradeCommand; +import seedu.address.logic.parser.exceptions.ParseException; + + + +/** + * Parses input arguments and creates a new GradeCommand object + */ +public class GradeCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the GradeCommand + * and returns an GradeCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public GradeCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_MODULE, PREFIX_GRADE); + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, GradeCommand.MESSAGE_USAGE), pe); + } + + List moduleNames = argMultimap.getAllValues(PREFIX_MODULE); + List gradeStrings = argMultimap.getAllValues(PREFIX_GRADE); + + if (moduleNames.isEmpty() || gradeStrings.isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, GradeCommand.MESSAGE_USAGE)); + } + + if (moduleNames.size() != gradeStrings.size()) { + throw new ParseException(String.format(MESSAGE_MISMATCH_MODULE_GRADE)); + } + + Map moduleGrades = new LinkedHashMap<>(); + for (int i = 0; i < moduleNames.size(); i++) { + String moduleName = moduleNames.get(i); + int grade = ParserUtil.parseGrade(gradeStrings.get(i)); + moduleGrades.put(moduleName, grade); + } + + return new GradeCommand(index, moduleGrades); + } +} diff --git a/src/main/java/seedu/address/logic/parser/LoadCommandParser.java b/src/main/java/seedu/address/logic/parser/LoadCommandParser.java new file mode 100644 index 00000000000..ef918c2f817 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/LoadCommandParser.java @@ -0,0 +1,42 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PATH; + +import java.nio.file.Path; +import java.util.stream.Stream; + +import seedu.address.logic.commands.LoadCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new LoadCommand object + */ +public class LoadCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the LoadCommand + * and returns an LoadCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public LoadCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_PATH); + + if (!arePrefixesPresent(argMultimap, PREFIX_PATH) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, LoadCommand.MESSAGE_USAGE)); + } + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PATH); + Path path = ParserUtil.parsePathWithCheck(argMultimap.getValue(PREFIX_PATH).get()); + return new LoadCommand(path); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..61b845e23e4 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,15 +2,20 @@ import static java.util.Objects.requireNonNull; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; import java.util.HashSet; import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.commands.ArchiveCommand; +import seedu.address.logic.commands.LoadCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; +import seedu.address.model.person.Gender; +import seedu.address.model.person.Module; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -21,6 +26,7 @@ public class ParserUtil { public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + public static final String MESSAGE_FILE_NOT_EXIST = "The target file is not a file or does not exits!"; /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be @@ -66,35 +72,45 @@ public static Phone parsePhone(String phone) throws ParseException { } /** - * Parses a {@code String address} into an {@code Address}. + * Parses a {@code String gender} into a {@code Gender}. * Leading and trailing whitespaces will be trimmed. * - * @throws ParseException if the given {@code address} is invalid. + * @throws ParseException if the given {@code Gender} 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); + public static Gender parseGender(String gender) throws ParseException { + requireNonNull(gender); + String trimmedGender = gender.trim(); + if (!Gender.isValidGender(trimmedGender)) { + throw new ParseException(Gender.MESSAGE_CONSTRAINTS); } - return new Address(trimmedAddress); + return new Gender(trimmedGender); } /** - * Parses a {@code String email} into an {@code Email}. + * Parses a {@code String module} into a {@code Module}. * Leading and trailing whitespaces will be trimmed. * - * @throws ParseException if the given {@code email} is invalid. + * @throws ParseException if the given {@code module} is invalid. */ - public static Email parseEmail(String email) throws ParseException { - requireNonNull(email); - String trimmedEmail = email.trim(); - if (!Email.isValidEmail(trimmedEmail)) { - throw new ParseException(Email.MESSAGE_CONSTRAINTS); + public static Module parseModule(String module) throws ParseException { + requireNonNull(module); + String trimmedModuleName = module.trim(); + if (!Module.isValidModule(trimmedModuleName) || !Module.isValidLength(trimmedModuleName)) { + throw new ParseException(Module.MESSAGE_CONSTRAINTS); } - return new Email(trimmedEmail); + return new Module(trimmedModuleName); + } + /** + * Parses {@code Collection modules} into a {@code Set}. + */ + public static Set parseModules(Collection modules) throws ParseException { + requireNonNull(modules); + final Set moduleSet = new HashSet<>(); + for (String module : modules) { + moduleSet.add(parseModule(module)); + } + return moduleSet; } - /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. @@ -121,4 +137,55 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + /** + * Parses a {@code String grade} into a {@code Grade}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code grade} is invalid. + */ + public static int parseGrade(String grade) throws ParseException { + requireNonNull(grade); + String trimmedGrade = grade.trim(); + try { + int numericGrade = Integer.parseInt(trimmedGrade); + if (!Module.isValidGrade(numericGrade)) { + throw new ParseException(Module.GRADE_CONSTRAINTS); + } + return numericGrade; + } catch (NumberFormatException e) { + throw new ParseException(Module.GRADE_CONSTRAINTS); + } + } + + /** + * Parses a {@code String path} into a {@code Path}. + * Leading and trailing whitespaces will be trimmed. + * */ + + public static Path parsePathWithCheck(String path) throws ParseException { + requireNonNull(path); + path = path.trim(); + final Path parsedPath = Paths.get("archived", path); + if (!path.endsWith(".json") || path.contains("/")) { + throw new ParseException(LoadCommand.MESSAGE_USAGE); + } else if (!Files.exists(parsedPath) || !Files.isRegularFile(parsedPath)) { + throw new ParseException(MESSAGE_FILE_NOT_EXIST); + } + return parsedPath; + } + + /** + * Parses a {@code String path} into a {@code Path}. + * Leading and trailing whitespaces will be trimmed. + * */ + + public static Path parsePathWithoutCheck(String path) throws ParseException { + requireNonNull(path); + path = path.trim(); + final Path parsedPath = Paths.get("archived", path); + if (!path.endsWith(".json") || path.contains("/")) { + throw new ParseException(ArchiveCommand.MESSAGE_USAGE); + } + return parsedPath; + } } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..489610b4033 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -84,4 +84,53 @@ public interface Model { * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + + /** + * Saves the current state of the address book. + * This allows the current state to be restored via undo or redo operations. + */ + void saveAddressBook(); + + /** + * Clears the current state of the address book. + * This clears all state in the undo/redo stack. + */ + void clearAddressBook(); + + /** + * Restores the previous state of the address book. + * This allows the user to undo the last change made to the address book. + * + * @throws seedu.address.model.VersionedAddressBook.InvalidUndoException if there is no previous state to undo to. + */ + void undoAddressBook(); + + /** + * Restores the next state of the address book that was undone. + * This allows the user to redo the last undone change. + * + * @throws seedu.address.model.VersionedAddressBook.InvalidRedoException if there is no state to redo to. + */ + void redoAddressBook(); + + /** + * Update and initializes the address book with a given address book. + * This clears and updates the entire undo/redo stack. + */ + void updateAddressBook(ReadOnlyAddressBook addressBook); + + /** + * Returns {@code true} if there are states to undo in the address book. + * + * @return {@code true} if undo is possible, {@code false} otherwise. + */ + boolean canUndoAddressBook(); + + /** + * Returns {@code true} if there are states to redo in the address book. + * + * @return {@code true} if redo is possible, {@code false} otherwise. + */ + boolean canRedoAddressBook(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..04bc949758a 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -15,25 +15,29 @@ /** * Represents the in-memory model of the address book data. + * This class manages the address book data and handles user preferences. */ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - private final AddressBook addressBook; + private final VersionedAddressBook versionedAddressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; /** - * Initializes a ModelManager with the given addressBook and userPrefs. + * Initializes a ModelManager with the given addressBook and user preferences. + * + * @param addressBook The address book to be managed. + * @param userPrefs The user preferences for the application. */ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { requireAllNonNull(addressBook, userPrefs); logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - this.addressBook = new AddressBook(addressBook); + this.versionedAddressBook = new VersionedAddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredPersons = new FilteredList<>(this.versionedAddressBook.getPersonList()); } public ModelManager() { @@ -79,28 +83,28 @@ public void setAddressBookFilePath(Path addressBookFilePath) { @Override public void setAddressBook(ReadOnlyAddressBook addressBook) { - this.addressBook.resetData(addressBook); + versionedAddressBook.resetData(addressBook); } @Override public ReadOnlyAddressBook getAddressBook() { - return addressBook; + return versionedAddressBook; } @Override public boolean hasPerson(Person person) { requireNonNull(person); - return addressBook.hasPerson(person); + return versionedAddressBook.hasPerson(person); } @Override public void deletePerson(Person target) { - addressBook.removePerson(target); + versionedAddressBook.removePerson(target); } @Override public void addPerson(Person person) { - addressBook.addPerson(person); + versionedAddressBook.addPerson(person); updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); } @@ -108,9 +112,10 @@ public void addPerson(Person person) { public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); - addressBook.setPerson(target, editedPerson); + versionedAddressBook.setPerson(target, editedPerson); } + //=========== Filtered Person List Accessors ============================================================= /** @@ -128,6 +133,51 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } + //=========== Undo and Redo ============================================================= + + @Override + public void undoAddressBook() { + versionedAddressBook.undo(); + updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + logger.info("Undid last action in address book."); + } + + @Override + public void updateAddressBook(ReadOnlyAddressBook addressBook) { + versionedAddressBook.resetData(addressBook); + versionedAddressBook.update(addressBook); + } + + @Override + public void redoAddressBook() { + versionedAddressBook.redo(); + updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + logger.info("Redid last action in address book."); + } + + @Override + public void saveAddressBook() { + versionedAddressBook.save(); + logger.info("Saved the current state of the address book."); + } + + @Override + public void clearAddressBook() { + versionedAddressBook.clear(); + this.setAddressBook(new AddressBook()); + logger.info("Cleared the address book."); + } + + @Override + public boolean canUndoAddressBook() { + return versionedAddressBook.canUndo(); + } + + @Override + public boolean canRedoAddressBook() { + return versionedAddressBook.canRedo(); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -140,7 +190,7 @@ public boolean equals(Object other) { } ModelManager otherModelManager = (ModelManager) other; - return addressBook.equals(otherModelManager.addressBook) + return versionedAddressBook.equals(otherModelManager.versionedAddressBook) && userPrefs.equals(otherModelManager.userPrefs) && filteredPersons.equals(otherModelManager.filteredPersons); } diff --git a/src/main/java/seedu/address/model/VersionedAddressBook.java b/src/main/java/seedu/address/model/VersionedAddressBook.java new file mode 100644 index 00000000000..dff355b6b2c --- /dev/null +++ b/src/main/java/seedu/address/model/VersionedAddressBook.java @@ -0,0 +1,162 @@ +package seedu.address.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@code VersionedAddressBook} stores multiple versions of {@code AddressBook} state, + * enabling undo and redo functionality. + */ +public class VersionedAddressBook extends AddressBook { + + private List addressBookStateList; + private int currentStatePointer; + + /** + * Initializes a new {@code VersionedAddressBook} with the initial state. + * + * @param initialState The initial state of the address book. + */ + public VersionedAddressBook(ReadOnlyAddressBook initialState) { + super(initialState); + + addressBookStateList = new ArrayList<>(); + AddressBook newState = new AddressBook(initialState); + addressBookStateList.add(newState); + currentStatePointer = 0; + } + + /** + * Saves the current state of the address book. + * Clears any redo history when called after undo. + */ + public void save() { + // Clear all states after the current pointer to prevent redo states + clearRedoStack(); + saveCurrentState(); + } + + /** + * Saves the current state as a new entry in the address book state list. + */ + private void saveCurrentState() { + AddressBook newState = new AddressBook(this); // Create a new state + addressBookStateList.add(newState); // Add the new state to the list + currentStatePointer++; // Move the pointer to the new current state + } + + /** + * Restores the {@code AddressBook} to the previous state. + * + * @throws InvalidUndoException if there is no state to undo to. + */ + public void undo() { + if (canUndo()) { + currentStatePointer--; + ReadOnlyAddressBook previousState = addressBookStateList.get(currentStatePointer); + resetData(previousState); + return; + } + throw new InvalidUndoException(); + } + + /** + * Restores the {@code AddressBook} to the next undone state. + * + * @throws InvalidRedoException if there is no state to redo to. + */ + public void redo() { + if (canRedo()) { + currentStatePointer++; + ReadOnlyAddressBook nextState = addressBookStateList.get(currentStatePointer); + resetData(nextState); + return; + } + throw new InvalidRedoException(); + } + + /** + * Clears all states from the Versioned Address Book. + */ + public void clear() { + this.addressBookStateList = new ArrayList<>(); + this.currentStatePointer = 0; + AddressBook newState = new AddressBook(); + addressBookStateList.add(newState); + } + + /** + * Updates the addressBookStateList with a new state - used exclusively by LoadCommand + * @param readOnlyAddressBook new state to be saved + */ + public void update(ReadOnlyAddressBook readOnlyAddressBook) { + this.addressBookStateList = new ArrayList<>(); + this.currentStatePointer = 0; + AddressBook newState = new AddressBook(readOnlyAddressBook); + addressBookStateList.add(newState); + } + + /** + * Clears all states after the current state to prevent redo. + * This is called when a new state is saved after an undo. + */ + private void clearRedoStack() { + // Remove all states beyond the current pointer + addressBookStateList.subList(currentStatePointer + 1, addressBookStateList.size()).clear(); + } + + /** + * Returns {@code true} if there are states to undo. + * + * @return {@code true} if undo is possible, {@code false} otherwise. + */ + public boolean canUndo() { + return currentStatePointer > 0; + } + + /** + * Returns {@code true} if there are states to redo. + * + * @return {@code true} if redo is possible, {@code false} otherwise. + */ + public boolean canRedo() { + int sizeOfStateList = addressBookStateList.size() - 1; + return currentStatePointer < sizeOfStateList; + } + + /** + * Returns the current state of the address book. + * + * @return the current {@code ReadOnlyAddressBook}. + */ + public ReadOnlyAddressBook getCurrentState() { + return addressBookStateList.get(currentStatePointer); // Return the current state + } + + /** + * Returns the total number of saved states. + * + * @return total count of saved states. + */ + public int getTotalStates() { + return addressBookStateList.size(); // Return the size of the state list + } + + /** + * Exception thrown when there is no state to undo to. + */ + public static class InvalidUndoException extends RuntimeException { + public InvalidUndoException() { + super("No available states to undo to."); + } + } + + /** + * Exception thrown when there is no state to redo to. + */ + public static class InvalidRedoException extends RuntimeException { + public InvalidRedoException() { + super("No available states to redo to."); + } + } +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index 469a2cc9a1e..00000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,65 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String VALIDATION_REGEX = "[^\\s].*"; - - public final String value; - - /** - * Constructs an {@code Address}. - * - * @param address A valid address. - */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; - } - - /** - * Returns true if a given string is a valid email. - */ - public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof Address)) { - return false; - } - - Address otherAddress = (Address) other; - return value.equals(otherAddress.value); - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/DetailContainsKeywordsPredicate.java similarity index 60% rename from src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java rename to src/main/java/seedu/address/model/person/DetailContainsKeywordsPredicate.java index 62d19be2977..4dcc701dc18 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/DetailContainsKeywordsPredicate.java @@ -9,17 +9,22 @@ /** * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. */ -public class NameContainsKeywordsPredicate implements Predicate { +public class DetailContainsKeywordsPredicate implements Predicate { private final List keywords; - public NameContainsKeywordsPredicate(List keywords) { + public DetailContainsKeywordsPredicate(List keywords) { this.keywords = keywords; } @Override public boolean test(Person person) { - return keywords.stream() + Boolean nameMatched = keywords.stream() .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + Boolean tagMatched = person.getTags().stream() + .anyMatch(tag -> keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(tag.tagName, keyword))); + + return nameMatched || tagMatched; } @Override @@ -29,12 +34,11 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof NameContainsKeywordsPredicate)) { + if (!(other instanceof DetailContainsKeywordsPredicate otherPredicate)) { return false; } - NameContainsKeywordsPredicate otherNameContainsKeywordsPredicate = (NameContainsKeywordsPredicate) other; - return keywords.equals(otherNameContainsKeywordsPredicate.keywords); + return keywords.equals(otherPredicate.keywords); } @Override diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java deleted file mode 100644 index c62e512bc29..00000000000 --- a/src/main/java/seedu/address/model/person/Email.java +++ /dev/null @@ -1,79 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's email in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} - */ -public class Email { - - private static final String SPECIAL_CHARACTERS = "+_.-"; - public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " - + "and adhere to the following constraints:\n" - + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " - + "the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " - + "characters.\n" - + "2. This is followed by a '@' and then a domain name. The domain name is made up of domain labels " - + "separated by periods.\n" - + "The domain name must:\n" - + " - end with a domain label at least 2 characters long\n" - + " - have each domain label start and end with alphanumeric characters\n" - + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any."; - // alphanumeric and special characters - private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]+"; // alphanumeric characters except underscore - private static final String LOCAL_PART_REGEX = "^" + ALPHANUMERIC_NO_UNDERSCORE + "([" + SPECIAL_CHARACTERS + "]" - + ALPHANUMERIC_NO_UNDERSCORE + ")*"; - private static final String DOMAIN_PART_REGEX = ALPHANUMERIC_NO_UNDERSCORE - + "(-" + ALPHANUMERIC_NO_UNDERSCORE + ")*"; - private static final String DOMAIN_LAST_PART_REGEX = "(" + DOMAIN_PART_REGEX + "){2,}$"; // At least two chars - private static final String DOMAIN_REGEX = "(" + DOMAIN_PART_REGEX + "\\.)*" + DOMAIN_LAST_PART_REGEX; - public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" + DOMAIN_REGEX; - - public final String value; - - /** - * Constructs an {@code Email}. - * - * @param email A valid email address. - */ - public Email(String email) { - requireNonNull(email); - checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); - value = email; - } - - /** - * Returns if a given string is a valid email. - */ - public static boolean isValidEmail(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof Email)) { - return false; - } - - Email otherEmail = (Email) other; - return value.equals(otherEmail.value); - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/FilterPredicate.java b/src/main/java/seedu/address/model/person/FilterPredicate.java new file mode 100644 index 00000000000..a6b8294e200 --- /dev/null +++ b/src/main/java/seedu/address/model/person/FilterPredicate.java @@ -0,0 +1,111 @@ +package seedu.address.model.person; + +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.FilterCommand.FilterPersonDescriptor; + +/** + * Tests that a {@code Person}'s details matches any of the keywords given. + */ +public class FilterPredicate implements Predicate { + private final FilterPersonDescriptor filterPersonDescriptor; + + public FilterPredicate(FilterPersonDescriptor filterPersonDescriptor) { + this.filterPersonDescriptor = filterPersonDescriptor; + } + + @Override + public boolean test(Person person) { + boolean nameMatched = filterName(person); + boolean phoneMatched = filterPhone(person); + boolean genderMatched = filterGender(person); + boolean modulesMatched = filterModules(person); + boolean tagsMatched = filterTags(person); + + return nameMatched && phoneMatched && genderMatched && modulesMatched && tagsMatched; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FilterPredicate otherPredicate)) { + return false; + } + + return filterPersonDescriptor.equals(otherPredicate.filterPersonDescriptor); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("conditions", filterPersonDescriptor) + .toString(); + } + + /** + * Filters a person based on the name specified in the filerPersonDescriptor. + * If the name in the filterPersonDescriptor is not present, + * this method will return true, indicating that the person passes the filter. + */ + private boolean filterName(Person person) { + return filterPersonDescriptor.getName() + .map(name -> StringUtil.containsWordIgnoreCase(person.getName().fullName, name.fullName)) + .orElse(true); + } + + /** + * Filters a person based on the phone specified in the filerPersonDescriptor. + * If the phone in the filterPersonDescriptor is not present, + * this method will return true, indicating that the person passes the filter. + */ + private boolean filterPhone(Person person) { + return filterPersonDescriptor.getPhone() + .map(phone -> StringUtil.containsNumber(person.getPhone().value, phone.value)) + .orElse(true); + } + + /** + * Filters a person based on the gender specified in the filerPersonDescriptor. + * If the gender in the filterPersonDescriptor is not present, + * this method will return true, indicating that the person passes the filter. + */ + private boolean filterGender(Person person) { + return filterPersonDescriptor.getGender() + .map(gender -> StringUtil.containsWordIgnoreCase(person.getGender().gender, gender.gender)) + .orElse(true); + } + + /** + * Filters a person based on the modules specified in the filerPersonDescriptor. + * If the modules in the filterPersonDescriptor is not present, + * this method will return true, indicating that the person passes the filter. + */ + private boolean filterModules(Person person) { + return filterPersonDescriptor.getModules() + .map(modules -> modules.stream() + .allMatch(module -> person.getModules().stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(module.module, keyword.module))) + ) + .orElse(true); + } + + /** + * Filters a person based on the tags specified in the filerPersonDescriptor. + * If the tags in the filterPersonDescriptor is not present, + * this method will return true, indicating that the person passes the filter. + */ + private boolean filterTags(Person person) { + return filterPersonDescriptor.getTags() + .map(tags -> tags.stream() + .allMatch(tag -> person.getTags().stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(tag.tagName, keyword.tagName))) + ) + .orElse(true); + } +} diff --git a/src/main/java/seedu/address/model/person/Gender.java b/src/main/java/seedu/address/model/person/Gender.java new file mode 100644 index 00000000000..ee2e3eb6dd0 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Gender.java @@ -0,0 +1,75 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Student's gender in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidGender(String)} + */ +public class Gender { + + public static final String MALE_SYMBOL = "♂"; + public static final String FEMALE_SYMBOL = "♀"; + public static final String MESSAGE_CONSTRAINTS = + "Gender should be either 'male' or 'female', and it should not be empty."; + + /* + * The first character of the gender should only be "male" or "female" + */ + public static final String VALIDATION_REGEX = "^(male|female)$"; + + public final String gender; + + /** + * Constructs a {@code Gender}. + * + * @param gender A valid gender. + */ + public Gender(String gender) { + requireNonNull(gender); + checkArgument(isValidGender(gender), MESSAGE_CONSTRAINTS); + this.gender = gender; + } + + /** + * Returns true if a given string is a valid name. + */ + public static boolean isValidGender(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Returns gender of person concatenated with its corresponding symbol. + */ + public String getGenderWithSymbol() { + return gender.equals("male") ? MALE_SYMBOL : FEMALE_SYMBOL; + } + + + @Override + public String toString() { + return gender; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Gender)) { + return false; + } + + Gender otherGender = (Gender) other; + return gender.equals(otherGender.gender); + } + + @Override + public int hashCode() { + return gender.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/Module.java b/src/main/java/seedu/address/model/person/Module.java new file mode 100644 index 00000000000..d6e9d64e6e3 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Module.java @@ -0,0 +1,123 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's Module in the address book + * Guarantees: immutable; is valid as declared in {@link #isValidModule(String)} + */ +public class Module { + public static final String MESSAGE_CONSTRAINTS = "Modules should consist of alphanumeric characters and " + + "spaces only, and it should be between 1 and 30 characters."; + public static final String GRADE_CONSTRAINTS = "Grade should be a number between 0 and 100."; + public static final String VALIDATION_REGEX = "^[\\p{Alnum} ]+$"; + + private static final int MIN_MODULE_LENGTH = 1; + private static final int MAX_MODULE_LENGTH = 30; + private static final int MIN_GRADE = 0; + private static final int MAX_GRADE = 100; + private static final int UNGRADED = -1; + public final String module; + private int grade; + private boolean isGraded; + /** + * Constructs an {@code Module}. + * + * @param module a valid Module. + */ + public Module(String module) { + requireNonNull(module); + checkArgument(isValidModule(module), MESSAGE_CONSTRAINTS); + checkArgument(isValidLength(module), MESSAGE_CONSTRAINTS); + this.module = module; + this.grade = UNGRADED; + this.isGraded = false; + } + + /** + * Constructs an {@code Module}. Only used in grading. + * + * @param module a valid Module. + */ + public Module(String module, String grade) { + requireNonNull(module); + checkArgument(isValidModule(module), MESSAGE_CONSTRAINTS); + checkArgument(isValidLength(module), MESSAGE_CONSTRAINTS); + this.module = module; + this.grade = grade.equals("Ungraded") ? UNGRADED : Integer.parseInt(grade); + this.isGraded = this.grade >= 0; + } + + /** + * Constructs an {@code Module} with grade. + * @param grade a valid grade (0 - 100). + */ + public void assignGrade(int grade) { + checkArgument(isValidGrade(grade), GRADE_CONSTRAINTS); + this.grade = grade; + this.isGraded = true; + } + /** + * Returns true if the module is graded. + */ + public boolean isGraded() { + return isGraded; + } + /** + * Returns true if a given integer is a valid grade. + * @param grade an integer. + * @return true if grade is a valid value, between (0 - 100). + */ + public static boolean isValidGrade(int grade) { + return (grade >= MIN_GRADE && grade <= MAX_GRADE) || grade == UNGRADED; + } + public String getGrade() { + return grade == UNGRADED ? "Ungraded" : String.valueOf(grade); + } + public String getModule() { + return module; + } + /** + * Returns true if a given string is a valid module. + */ + public static boolean isValidModule(String test) { + return test.trim().matches(VALIDATION_REGEX); + } + + /** + * Checks if the input value is of valid length. + * @param test the module value to be tested. + * @return the result of the test. + */ + public static boolean isValidLength(String test) { + boolean isValidMinLength = test.length() >= MIN_MODULE_LENGTH; + boolean isValidMaxLength = test.length() <= MAX_MODULE_LENGTH; + return isValidMinLength && isValidMaxLength; + } + + @Override + public String toString() { + return '[' + module + " | Grade: " + (grade == UNGRADED ? "Ungraded" : grade) + ']'; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Module)) { + return false; + } + + Module otherModule = (Module) other; + return module.equals(otherModule.module); + } + + @Override + public int hashCode() { + return module.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..82e32d112c1 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -10,13 +10,16 @@ public class Name { public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + "Names should only contain alphabets, hyphens, dots, commas, forward slash" + + "and spaces, and be between 1 and 100 characters."; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "^(?=.*[\\p{L}\\p{N}])[\\p{L}\\-\\., /]{1,100}$"; + private static final int MIN_LENGTH = 1; + private static final int MAX_LENGTH = 100; public final String fullName; @@ -28,6 +31,7 @@ public class Name { public Name(String name) { requireNonNull(name); checkArgument(isValidName(name), MESSAGE_CONSTRAINTS); + checkArgument(isValidLength(name), MESSAGE_CONSTRAINTS); fullName = name; } @@ -38,12 +42,28 @@ public static boolean isValidName(String test) { return test.matches(VALIDATION_REGEX); } + /** + * Checks the length of the name is valid. + * @param test the name to be tested. + * @return the result of the test. + */ + private boolean isValidLength(String test) { + if (test.trim().isEmpty()) { + return false; + } else { + return test.length() <= MAX_LENGTH; + } + } @Override public String toString() { return fullName; } + public boolean isSameName(Name otherName) { + return this.fullName.equalsIgnoreCase(otherName.fullName); + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..4c2efa032a5 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -19,22 +19,22 @@ public class Person { // Identity fields private final Name name; private final Phone phone; - private final Email email; + private final Gender gender; // Data fields - private final Address address; + private final Set modules = new HashSet<>(); private final Set tags = new HashSet<>(); /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Phone phone, Gender gender, Set modules, Set tags) { + requireAllNonNull(name, phone, gender, modules, tags); this.name = name; this.phone = phone; - this.email = email; - this.address = address; + this.modules.addAll(modules); this.tags.addAll(tags); + this.gender = gender; } public Name getName() { @@ -45,12 +45,12 @@ public Phone getPhone() { return phone; } - public Email getEmail() { - return email; + public Gender getGender() { + return gender; } - public Address getAddress() { - return address; + public Set getModules() { + return Collections.unmodifiableSet(modules); } /** @@ -71,7 +71,7 @@ public boolean isSamePerson(Person otherPerson) { } return otherPerson != null - && otherPerson.getName().equals(getName()); + && otherPerson.getName().isSameName(getName()); } /** @@ -92,15 +92,14 @@ public boolean equals(Object other) { Person otherPerson = (Person) other; return name.equals(otherPerson.name) && phone.equals(otherPerson.phone) - && email.equals(otherPerson.email) - && address.equals(otherPerson.address) + && modules.equals(otherPerson.modules) && tags.equals(otherPerson.tags); } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, modules, tags); } @Override @@ -108,8 +107,8 @@ public String toString() { return new ToStringBuilder(this) .add("name", name) .add("phone", phone) - .add("email", email) - .add("address", address) + .add("gender", gender) + .add("modules", modules) .add("tags", tags) .toString(); } diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d733f63d739..7ba399af9fd 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -9,10 +9,11 @@ */ public class Phone { - public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String VALIDATION_REGEX = "\\d{3,}"; + "Phone numbers should only contain numbers and be exactly 8 digits long."; + public static final String VALIDATION_REGEX = "\\d{8}$"; + + private static final int PHONE_LENGTH = 8; public final String value; /** @@ -23,6 +24,7 @@ public class Phone { public Phone(String phone) { requireNonNull(phone); checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); + checkArgument(isLengthValid(phone), MESSAGE_CONSTRAINTS); value = phone; } @@ -33,6 +35,15 @@ public static boolean isValidPhone(String test) { return test.matches(VALIDATION_REGEX); } + /** + * Returns true if a given string has a valid length of 8. + * @param test the phone string to be tested. + * @return result of the check. + */ + public boolean isLengthValid(String test) { + return test.length() == PHONE_LENGTH; + } + @Override public String toString() { return value; diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index f1a0d4e233b..fe3a317d037 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -9,8 +9,10 @@ */ public class Tag { - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; + public static final String MESSAGE_CONSTRAINTS = + "Tags names should be alphanumeric, and be between 1 and 30 characters."; public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + private static final int MAX_TAG_LENGTH = 30; public final String tagName; @@ -22,6 +24,7 @@ public class Tag { public Tag(String tagName) { requireNonNull(tagName); checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); + checkArgument(isValidLength(tagName), MESSAGE_CONSTRAINTS); this.tagName = tagName; } @@ -32,6 +35,15 @@ public static boolean isValidTagName(String test) { return test.matches(VALIDATION_REGEX); } + /** + * Checks if the test tag is valid. + * @param test the tag to be tested. + * @return the result of the test + */ + private boolean isValidLength(String test) { + return test.length() <= MAX_TAG_LENGTH; + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..f8eda048a9b 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -6,8 +6,8 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; +import seedu.address.model.person.Gender; +import seedu.address.model.person.Module; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -19,24 +19,26 @@ public class SampleDataUtil { public static Person[] getSamplePersons() { return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Gender("male"), + getModuleSet("Chemistry"), getTagSet("new", "IB", "smart")), + new Person(new Name("Bernice Yu"), new Phone("99272758"), new Gender("female"), + getModuleSet("History"), getTagSet("smart")), + new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Gender("female"), + getModuleSet("English"), getTagSet("OLevels")), + new Person(new Name("David Li"), new Phone("91031282"), new Gender("male"), + getModuleSet("English"), getTagSet("OLevels", "smart", "new")), + new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Gender("male"), + getModuleSet("Mathematics"), getTagSet("ALevels", "smart")), + new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Gender("male"), + getModuleSet("Tamil"), getTagSet("new", "smart")), + new Person(new Name("Evelyn Tan"), new Phone("81234567"), new Gender("female"), + getModuleSet("Chinese"), getTagSet("IB", "smart")), + new Person(new Name("Farah Ahmed"), new Phone("87987654"), new Gender("male"), + getModuleSet("Malay"), getTagSet("NLevels")), + new Person(new Name("Gabriel Lim"), new Phone("89876543"), new Gender("male"), + getModuleSet("Higher Chinese"), getTagSet("help")), + new Person(new Name("Hannah Koh"), new Phone("89765432"), new Gender("female"), + getModuleSet("Literature"), getTagSet("new")) }; } @@ -57,4 +59,13 @@ public static Set getTagSet(String... strings) { .collect(Collectors.toSet()); } + /** + * Returns a module set containing the list of strings given. + */ + public static Set getModuleSet(String... strings) { + return Arrays.stream(strings) + .map(Module::new) + .collect(Collectors.toSet()); + } + } diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/seedu/address/storage/AddressBookStorage.java index f2e015105ae..d616bb89022 100644 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ b/src/main/java/seedu/address/storage/AddressBookStorage.java @@ -42,4 +42,13 @@ public interface AddressBookStorage { */ void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException; + /** + * Archive the given {@link ReadOnlyAddressBook} to the storage. + * @param addressBook cannot be null. + * @param filePath the path to archive, can not be null + * @throws IOException if there was any problem writing to the file. + */ + + void saveArchivedAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException; + } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedModule.java b/src/main/java/seedu/address/storage/JsonAdaptedModule.java new file mode 100644 index 00000000000..877ddf86551 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedModule.java @@ -0,0 +1,70 @@ +package seedu.address.storage; + +import static seedu.address.storage.JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Module; + + +/** + * Jackson-friendly version of {@link Module}. + */ +class JsonAdaptedModule { + + private final String module; + private Integer grade; + + /** + * Constructs a {@code JsonAdaptedModule} with the given {@code module}. + */ + @JsonCreator + public JsonAdaptedModule(@JsonProperty("module") String module, @JsonProperty("grade") Integer grade) { + this.module = module; + this.grade = grade; + } + + /** + * Converts a given {@code Module} into this class for Jackson use. + */ + public JsonAdaptedModule(Module source) { + module = source.module; + if ("Ungraded".equals(source.getGrade())) { + grade = -1; + } else { + grade = Integer.parseInt(source.getGrade()); + } + } + public String getModule() { + return module; + } + public int getGrade() { + return grade; + } + /** + * Converts this Jackson-friendly adapted module object into the model's {@code Module} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted module. + */ + public Module toModelType() throws IllegalValueException { + if (module == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Module.class.getSimpleName())); + } + + if (grade == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "Grade")); + } + + if (!Module.isValidModule(module)) { + throw new IllegalValueException(Module.MESSAGE_CONSTRAINTS); + } + if (!Module.isValidGrade(grade)) { + throw new IllegalValueException(Module.GRADE_CONSTRAINTS); + } + Module moduleObject = new Module(module); + moduleObject.assignGrade(grade); + return moduleObject; + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..92957eecece 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -10,8 +10,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; +import seedu.address.model.person.Gender; +import seedu.address.model.person.Module; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -26,8 +26,8 @@ class JsonAdaptedPerson { private final String name; private final String phone; - private final String email; - private final String address; + private final String gender; + private final List modules = new ArrayList<>(); private final List tags = new ArrayList<>(); /** @@ -35,12 +35,15 @@ class JsonAdaptedPerson { */ @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("gender") String gender, + @JsonProperty("modules") List modules, + @JsonProperty("tags") List tags) { this.name = name; this.phone = phone; - this.email = email; - this.address = address; + this.gender = gender; + if (modules != null) { + this.modules.addAll(modules); + } if (tags != null) { this.tags.addAll(tags); } @@ -52,8 +55,12 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone public JsonAdaptedPerson(Person source) { name = source.getName().fullName; phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; + gender = source.getGender().gender; + modules.addAll(source.getModules().stream() + .map(module -> new JsonAdaptedModule( + module.getModule(), + "Ungraded".equals(module.getGrade()) ? -1 : Integer.parseInt(module.getGrade()))) + .collect(Collectors.toList())); tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); @@ -70,6 +77,14 @@ public Person toModelType() throws IllegalValueException { personTags.add(tag.toModelType()); } + final List personModules = new ArrayList<>(); + for (JsonAdaptedModule module : modules) { + personModules.add(module.toModelType()); + } + + if (personModules.isEmpty()) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Module.class.getSimpleName())); + } if (name == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); } @@ -86,24 +101,15 @@ public Person toModelType() throws IllegalValueException { } final Phone modelPhone = new Phone(phone); - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); + if (gender == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Gender.class.getSimpleName())); } - if (!Email.isValidEmail(email)) { - throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); + if (!Gender.isValidGender(gender)) { + throw new IllegalValueException(Gender.MESSAGE_CONSTRAINTS); } - final Email modelEmail = new Email(email); - - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); - } - final Address modelAddress = new Address(address); - + final Gender modelGender = new Gender(gender); + final Set modelModules = new HashSet<>(personModules); final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + return new Person(modelName, modelPhone, modelGender, modelModules, modelTags); } - } diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java index 41e06f264e1..a9cd6147261 100644 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java @@ -23,6 +23,10 @@ public class JsonAddressBookStorage implements AddressBookStorage { private Path filePath; + /** + * Create a JsonAddressBookStorage with file Path and default archive path + * @param filePath the file path. + * */ public JsonAddressBookStorage(Path filePath) { this.filePath = filePath; } @@ -77,4 +81,14 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro JsonUtil.saveJsonFile(new JsonSerializableAddressBook(addressBook), filePath); } + + @Override + public void saveArchivedAddressBook(ReadOnlyAddressBook addressBook, Path archivePath) throws IOException { + requireNonNull(addressBook); + requireNonNull(archivePath); + + FileUtil.createIfMissing(archivePath); + JsonUtil.saveJsonFile(new JsonSerializableAddressBook(addressBook), archivePath); + } + } diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java index 48a9754807d..c820e4f61ef 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java @@ -20,6 +20,7 @@ public JsonUserPrefsStorage(Path filePath) { this.filePath = filePath; } + @Override public Path getUserPrefsFilePath() { return filePath; diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index 8b84a9024d5..7443b9240a7 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -75,4 +75,18 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro addressBookStorage.saveAddressBook(addressBook, filePath); } + @Override + public void saveArchivedAddressBook(ReadOnlyAddressBook addressBook, Path archivedPath) throws IOException { + logger.fine("Attempting to write to data file: " + archivedPath); + addressBookStorage.saveArchivedAddressBook(addressBook, archivedPath); + } + + /** + * read from the archived file + * */ + public Optional readArchivedAddressBook(Path archivePath) throws DataLoadingException { + logger.fine("Attempting to read data from file: " + archivePath); + return addressBookStorage.readAddressBook(archivePath); + } + } diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/seedu/address/storage/UserPrefsStorage.java index e94ca422ea8..6940aef275d 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/seedu/address/storage/UserPrefsStorage.java @@ -18,6 +18,7 @@ public interface UserPrefsStorage { */ Path getUserPrefsFilePath(); + /** * Returns UserPrefs data from storage. * Returns {@code Optional.empty()} if storage file is not found. diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 9e75478664b..18f34576f8a 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -1,36 +1,110 @@ package seedu.address.ui; +import java.awt.Dimension; +import java.awt.Toolkit; +import java.util.HashMap; + import javafx.collections.ObservableList; import javafx.fxml.FXML; +import javafx.geometry.Bounds; +import javafx.geometry.Side; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; +import seedu.address.logic.Logic; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.AutocompleteParser; import seedu.address.logic.parser.exceptions.ParseException; /** * The UI component that is responsible for receiving user command inputs. */ public class CommandBox extends UiPart { - public static final String ERROR_STYLE_CLASS = "error"; private static final String FXML = "CommandBox.fxml"; + private static final int BORDER_SIZE = 10; + private static final int ITEM_SIZE = 30; + private static final double ITEM_PADDING = 1.5; private final CommandExecutor commandExecutor; + private final AutocompleteParser autocompleteParser; @FXML private TextField commandTextField; + @FXML + private ContextMenu autoComplete; /** * Creates a {@code CommandBox} with the given {@code CommandExecutor}. */ - public CommandBox(CommandExecutor commandExecutor) { + public CommandBox(CommandExecutor commandExecutor, Logic logic) { super(FXML); this.commandExecutor = commandExecutor; + this.autocompleteParser = new AutocompleteParser(); // calls #setStyleToDefault() whenever there is a change to the text of the command box. commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); + + commandTextField.caretPositionProperty().addListener((obs, oldPosition, newPosition) -> { + // Call method to update and show the autocomplete suggestions + updateAutoComplete(autocompleteParser.parseCommand(commandTextField.getText(), logic.getAddressBook(), + newPosition.intValue())); + }); } + /** + * Updates the autocomplete context menu with suggestions based on the current input. + */ + private void updateAutoComplete(HashMap suggestions) { + autoComplete.getItems().clear(); + + if (suggestions.isEmpty()) { + autoComplete.hide(); + return; + } + + for (HashMap.Entry suggestion : suggestions.entrySet()) { + MenuItem item = new MenuItem(suggestion.getKey()); + item.setOnAction(e -> { + commandTextField.setText(suggestion.getValue()); + commandTextField.positionCaret(suggestion.getValue().length()); + autoComplete.hide(); + }); + autoComplete.getItems().add(item); + + autoComplete.setMaxHeight(150); + autoComplete.setMaxWidth(400); + autoComplete.setAutoFix(false); + + autoComplete.show(commandTextField, Side.BOTTOM, 0, 0); + + // To solve weird JavaFX behavior where scrolling position is not updated after items have changed. + autoComplete.getSkin().getNode().requestFocus(); + commandTextField.fireEvent(new KeyEvent( + KeyEvent.KEY_PRESSED, "", "", + KeyCode.DOWN, false, false, false, false)); + } + Bounds boundsInScreen = commandTextField.localToScreen(commandTextField.getBoundsInLocal()); + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + double height = screenSize.getHeight(); + double autocompleteHeight = Math.min(suggestions.size(), 5) * ITEM_SIZE + + BORDER_SIZE + - Math.min(suggestions.size() - 1, 5) * ITEM_PADDING; + + // Position suggestion box above text field if there is insufficient space below the text box + if (height - boundsInScreen.getMaxY() < autocompleteHeight) { + autoComplete.show(commandTextField, Side.BOTTOM, 0, -autocompleteHeight - commandTextField.getHeight()); + autoComplete.setOpacity(0.8); + } else { + autoComplete.show(commandTextField, Side.BOTTOM, 0, 0); + autoComplete.setOpacity(1); + } + } + + /** * Handles the Enter button pressed event. */ diff --git a/src/main/java/seedu/address/ui/DialogBox.java b/src/main/java/seedu/address/ui/DialogBox.java new file mode 100644 index 00000000000..8d9e55bcf8c --- /dev/null +++ b/src/main/java/seedu/address/ui/DialogBox.java @@ -0,0 +1,91 @@ +package seedu.address.ui; + +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.shape.Rectangle; + +/** + * Represents a dialog box consisting of an ImageView to represent the speaker's face + * and a label containing text from the speaker. + */ +public class DialogBox extends HBox { + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + private DialogBox(String text, Image img) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("/view/DialogBox.fxml")); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + Rectangle rectangle = new Rectangle(0, 0, 150, 150); + dialog.setText(text); + displayPicture.setImage(img); + displayPicture.setClip(rectangle); + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); + dialog.getStyleClass().add("reply-label"); + } + + /** + * Turns the dialog box into an error box message. + */ + private void makeErrorMessage() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + getChildren().setAll(tmp); + dialog.getStyleClass().add("error-label"); + } + + /** + * Returns a user dialog box message. + * + * @param text Command text inputted by user. + * @param img Image to be used by the dialog box. + * @param isError Whether the command resulted in an error. + */ + public static DialogBox getUserDialog(String text, Image img, boolean isError) { + var db = new DialogBox(text, img); + if (isError) { + db.makeErrorMessage(); + } + return db; + } + + /** + * Returns a chatbot dialog box message. + * + * @param text Command text inputted by user. + * @param img Image to be used by the dialog box. + */ + public static DialogBox getChatBotDialog(String text, Image img) { + var db = new DialogBox(text, img); + db.flip(); + + return db; + } +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..daae77c2c9d 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2425s1-cs2103t-w10-4.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..d403ca3f9b2 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -115,11 +115,12 @@ void fillInnerParts() { resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); + resultDisplay.greet("Welcome to StoreClass!"); StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - CommandBox commandBox = new CommandBox(this::executeCommand); + CommandBox commandBox = new CommandBox(this::executeCommand, this.logic); commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); } @@ -176,7 +177,7 @@ private CommandResult executeCommand(String commandText) throws CommandException try { CommandResult commandResult = logic.execute(commandText); logger.info("Result: " + commandResult.getFeedbackToUser()); - resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + resultDisplay.setFeedbackToUser(commandText, commandResult.getFeedbackToUser(), false); if (commandResult.isShowHelp()) { handleHelp(); @@ -189,7 +190,7 @@ private CommandResult executeCommand(String commandText) throws CommandException return commandResult; } catch (CommandException | ParseException e) { logger.info("An error occurred while executing command: " + commandText); - resultDisplay.setFeedbackToUser(e.getMessage()); + resultDisplay.setFeedbackToUser(commandText, e.getMessage(), true); throw e; } } diff --git a/src/main/java/seedu/address/ui/MaxSizedContextMenu.java b/src/main/java/seedu/address/ui/MaxSizedContextMenu.java new file mode 100644 index 00000000000..6bcd7f736cc --- /dev/null +++ b/src/main/java/seedu/address/ui/MaxSizedContextMenu.java @@ -0,0 +1,36 @@ +package seedu.address.ui; + +import javafx.scene.Node; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Menu; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Region; + +/** + * Modified context menu component that has scrolling mechanism if max height is exceeded + */ +public class MaxSizedContextMenu extends ContextMenu { + public static final int CONTEXT_MENU_MAX_HEIGHT = 150; + public static final int CONTEXT_MENU_MAX_WIDTH = 400; + + /** + * Constructor for max sized context menu + */ + public MaxSizedContextMenu() { + addEventHandler(Menu.ON_SHOWING, e -> { + Node content = getSkin().getNode(); + if (content instanceof Region regionContent) { + regionContent.setMaxHeight(CONTEXT_MENU_MAX_HEIGHT); + regionContent.setMaxWidth(CONTEXT_MENU_MAX_WIDTH); + + // To solve weird JavaFX behavior where scrolling position is not updated after items have + // changed. + content.requestFocus(); + this.fireEvent(new KeyEvent( + KeyEvent.KEY_PRESSED, "", "", + KeyCode.UP, false, false, false, false)); + } + }); + } +} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..2c271e2f9d1 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -2,11 +2,14 @@ import java.util.Comparator; +import javafx.beans.binding.Bindings; import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.scene.paint.Color; import seedu.address.model.person.Person; /** @@ -33,13 +36,15 @@ public class PersonCard extends UiPart { @FXML private Label id; @FXML + private Label gender; + @FXML private Label phone; @FXML - private Label address; + private FlowPane tags; @FXML - private Label email; + private FlowPane modules; @FXML - private FlowPane tags; + private FlowPane grades; /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. @@ -49,11 +54,53 @@ public PersonCard(Person person, int displayedIndex) { this.person = person; id.setText(displayedIndex + ". "); name.setText(person.getName().fullName); + gender.setText(person.getGender().getGenderWithSymbol()); phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + person.getModules().stream() + .sorted(Comparator.comparing(module -> module.module)) + .forEach(moduleCode -> { + Label moduleLabel = new Label(moduleCode.module); + + // Color-coding based on grade + String gradeString = moduleCode.getGrade(); + int gradeValue = 0; // Default to 0 if parsing is needed + + if (gradeString.equalsIgnoreCase("Ungraded")) { + // Set style for ungraded modules + moduleLabel.setStyle("-fx-background-color: #B0BEC5; -fx-background-radius: 5; " + + "-fx-text-fill: black; -fx-padding: 5 10; -fx-font-weight: bold;"); + moduleLabel.setTooltip(new Tooltip(moduleCode.module + " (Ungraded)")); + } else { + try { + gradeValue = Integer.parseInt(gradeString); + // Set color based on grade range + if (gradeValue >= 50) { + moduleLabel.setStyle("-fx-background-color: #4CAF50; -fx-background-radius: 5; " + + "-fx-text-fill: white; -fx-padding: 5 10; -fx-font-weight: bold;"); + } else { + moduleLabel.setStyle("-fx-background-color: #F44336; -fx-background-radius: 5; " + + "-fx-text-fill: white; -fx-padding: 5 10; -fx-font-weight: bold;"); + } + // Tooltip for graded modules + moduleLabel.setTooltip(new Tooltip("Module: " + moduleCode.module + "\nGrade: " + + gradeValue)); + } catch (NumberFormatException e) { + // Handle unexpected non-numeric grades gracefully + moduleLabel.setStyle("-fx-background-color: #B0BEC5; -fx-background-radius: 5; " + + "-fx-text-fill: black; -fx-padding: 5 10; -fx-font-weight: bold;"); + moduleLabel.setTooltip(new Tooltip(moduleCode.module + " (Invalid grade)")); + } + } + + // Add the moduleLabel directly to the modules FlowPane + modules.getChildren().add(moduleLabel); + }); + gender.textFillProperty().bind( + Bindings.when(gender.textProperty().isEqualTo("♂")) + .then(Color.LIGHTBLUE) + .otherwise(Color.PINK)); } } diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/address/ui/ResultDisplay.java index 7d98e84eedf..a8af1b527f0 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/address/ui/ResultDisplay.java @@ -1,10 +1,11 @@ package seedu.address.ui; -import static java.util.Objects.requireNonNull; - import javafx.fxml.FXML; +import javafx.scene.control.ScrollPane; import javafx.scene.control.TextArea; +import javafx.scene.image.Image; import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; /** * A ui for the status bar that is displayed at the header of the application. @@ -13,16 +14,51 @@ public class ResultDisplay extends UiPart { private static final String FXML = "ResultDisplay.fxml"; + private final Image userImage = new Image(this.getClass().getResourceAsStream("/images/userimage.png")); + private final Image chatBotImage = new Image(this.getClass().getResourceAsStream("/images/storeclass.png")); + @FXML private TextArea resultDisplay; + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; public ResultDisplay() { super(FXML); } - public void setFeedbackToUser(String feedbackToUser) { - requireNonNull(feedbackToUser); - resultDisplay.setText(feedbackToUser); + /** + * Bind vvalue property of scroll pane so that it scrolls to the bottom after each command. + */ + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); } + /** + * Outputs a feedback message in the chat panel in response to a user command. + * + * @param commandText Command sent by user. + * @param feedbackToUser Response of chatbot to user. + * @param isError Whether the command resulted in an error. + */ + public void setFeedbackToUser(String commandText, String feedbackToUser, boolean isError) { + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(commandText, userImage, isError), + DialogBox.getChatBotDialog(feedbackToUser, chatBotImage) + ); + } + + /** + * Outputs a greeting message from chatbot. + * + * @param message Message for chatbot to greet with. + */ + @FXML + public void greet(String message) { + dialogContainer.getChildren().addAll( + DialogBox.getChatBotDialog(message, chatBotImage) + ); + } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..1f2055b0565 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/storeclass.png"; private Logic logic; private MainWindow mainWindow; diff --git a/src/main/resources/images/storeclass.png b/src/main/resources/images/storeclass.png new file mode 100644 index 00000000000..2b326ac4fcf Binary files /dev/null and b/src/main/resources/images/storeclass.png differ diff --git a/src/main/resources/images/userimage.png b/src/main/resources/images/userimage.png new file mode 100644 index 00000000000..d60aea7d62f Binary files /dev/null and b/src/main/resources/images/userimage.png differ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 124283a392e..f161f257d76 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -2,8 +2,13 @@ + - + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..a757b353d91 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -76,15 +76,18 @@ -fx-background-color: -fx-focus-color; } -.split-pane:horizontal .split-pane-divider { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: transparent transparent transparent #4d4d4d; +.split-pane > .split-pane-divider { + -fx-background-color: #18181B; + -fx-border-color: #18181B; + -fx-border-width: 2; + -fx-padding: 0; } .split-pane { - -fx-border-radius: 1; - -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-border-radius: 0; + -fx-border-width: 0; + -fx-padding: 0; + -fx-background-color: #18181B; } .list-view { @@ -107,6 +110,11 @@ -fx-background-color: #515658; } +.list-cell:filled:first { + -fx-border-radius: 5; + -fx-background-radius: 5; +} + .list-cell:filled:selected { -fx-background-color: #424d5f; } @@ -122,8 +130,9 @@ .cell_big_label { -fx-font-family: "Segoe UI Semibold"; - -fx-font-size: 16px; + -fx-font-size: 20px; -fx-text-fill: #010504; + -fx-font-weight: bold; } .cell_small_label { @@ -133,17 +142,23 @@ } .stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #27272a; } .pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); + -fx-background-color: #27272a; + -fx-border-color: #27272a; + -fx-border-top-width: 1px; +} + +.pane-with-border .scrollPane { + -fx-background-color: #27272a; + -fx-border-color: #27272a; -fx-border-top-width: 1px; } .status-bar { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: #18181B; } .result-display { @@ -159,9 +174,9 @@ .status-bar .label { -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: #E0F2FE; -fx-padding: 4px; - -fx-pref-height: 30px; + -fx-pref-height: 20px; } .status-bar-with-border { @@ -189,17 +204,16 @@ } .context-menu .label { - -fx-text-fill: white; + -fx-text-fill: #E0F2FE; } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #18181B; } .menu-bar .label { - -fx-font-size: 14pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-font: bold 12pt "Inter"; + -fx-text-fill: #E0F2FE; -fx-opacity: 0.9; } @@ -282,11 +296,11 @@ } .scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: transparent; } .scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: #0F172A; -fx-background-insets: 3; } @@ -308,33 +322,41 @@ } #cardPane { - -fx-background-color: transparent; - -fx-border-width: 0; + -fx-background-color: #334155; + -fx-border-width: 0.5; + -fx-border-color: black; } #commandTypeLabel { -fx-font-size: 11px; - -fx-text-fill: #F70D1A; + -fx-text-fill: #E0F2FE; } #commandTextField { - -fx-background-color: transparent #383838 transparent #383838; + -fx-background-color: #1E293B; -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; + -fx-border-color: transparent; -fx-border-insets: 0; - -fx-border-width: 1; + -fx-border-width: 0; -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; - -fx-text-fill: white; + -fx-font-size: 14pt; + -fx-prompt-text-fill: #E0F2FE; + -fx-text-fill: #E0F2FE; } #filterField, #personListPanel, #personWebpage { -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); } -#resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; +.scroll-pane { + -fx-background-color: #475569; -fx-background-radius: 0; + -fx-border-radius: 0; +} + +.scroll-pane .viewport { + -fx-background-color: #475569; + -fx-border-radius: 0; } #tags { @@ -350,3 +372,32 @@ -fx-background-radius: 2; -fx-font-size: 11; } + +#modules .label { + -fx-text-fill: black; + -fx-font-weight: bold; + -fx-font-family: "Noto Sans"; + -fx-background-color: #ea580c; + -fx-padding: 7; + -fx-border-insets: 3; + -fx-background-insets: 3; + -fx-border-radius: 20; + -fx-background-radius: 20; + -fx-font-size: 16; +} + +#personListLabel { + -fx-background-color: #0F172A; + -fx-padding: 20; +} + +#personListLabel .label { + -fx-text-fill: #E0F2FE; + -fx-font-size: 20px; + -fx-font-weight: bold; +} + +#autoComplete { + -fx-background-color: derive(#1d1d1d, 35%); + -fx-border-color: #27272a; +} diff --git a/src/main/resources/view/DialogBox.css b/src/main/resources/view/DialogBox.css new file mode 100644 index 00000000000..63fcc52cad2 --- /dev/null +++ b/src/main/resources/view/DialogBox.css @@ -0,0 +1,41 @@ +.label { + -fx-background-color: #1E293B; + -fx-border-color: #0F172A; + -fx-border-width: 1px; + -fx-background-radius: 1em; + -fx-border-radius: 1em; + -fx-font: medium 1.25em "Inter"; + -fx-text-fill: #16a34a; +} + +.reply-label { + -fx-text-fill: #E0F2FE; + -fx-background-color: #1E293B; + -fx-border-color: #0F172A; + -fx-background-radius: 1em 1em 1em 0; + -fx-border-radius: 1em 1em 1em 0; +} + +.error-label { + -fx-background-color: #1E293B; + -fx-border-color: #0F172A; + -fx-border-width: 1px; + -fx-background-radius: 1em; + -fx-border-radius: 1em; + -fx-font: medium 1.25em "Inter"; + -fx-text-fill: #dc2626; +} + +#displayPicture { + /* Shadow effect on image. */ + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 10, 0.5, 5, 5); + + /* Change size of image. */ + -fx-scale-x: 1; + -fx-scale-y: 1; + + /* Rotate image clockwise by degrees. */ + -fx-rotate: 0; + -fx-border-radius: 100%; + -fx-background-radius: 50px; +} diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 00000000000..f075c81a4db --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..35d6aefd1ae 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -1,7 +1,6 @@ - @@ -12,9 +11,9 @@ + title="StoreClass" minWidth="800" minHeight="700" onCloseRequest="#handleExit"> - + @@ -33,26 +32,15 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index 84e09833a87..bbc67f1e812 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -7,30 +7,47 @@ + - + - - - - - - + + + + diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml index a1bb6bbace8..0744dbfb882 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/PersonListPanel.fxml @@ -2,7 +2,12 @@ + + + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 01b691792a9..37d2da70416 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -1,9 +1,11 @@ - - + + - -