diff --git a/README.md b/README.md index 16208adb9b6..bcbc70fa83a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,84 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![CI Status](https://github.com/AY2425S2-CS2103T-F14-3/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2425S2-CS2103T-F14-3/tp/actions) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org/#contributing-to-se-edu) for more info. +# RecruitIntel + +## Overview + +RecruitIntel is a **desktop application designed specifically for Apple's HR recruiters** to efficiently manage candidate information. While offering an easy-to-use visual interface, it features powerful text commands that help process candidate information considerably faster than traditional mouse-based applications. + +## Features + +* **Candidate Management** + * Add candidates with detailed information + * Edit existing candidate records + * Delete candidate records + * List all candidates in the system + +* **Advanced Categorization** + * Classify candidates by skills, teams, and positions + * Tag candidates with relevant skills and attributes + * Find candidates by name with partial matching + +* **Interview Management** + * Schedule interviews with specific times and durations + * Sort candidates by interview times + * Add detailed notes about interview performance + +* **User Experience** + * Data is automatically saved + * Fast command-based interface + * Undo/redo functionality for most operations + +## Quick Start + +1. Ensure you have Java `17` or above installed. +2. Download the latest `RecruitIntel.jar` from the [releases page](https://github.com/AY2425S2-CS2103T-F14-3/tp/releases). +3. Create a dedicated folder for RecruitIntel. +4. Copy the downloaded JAR file into this folder. +5. Run the application: + ``` + java -jar recruitintel.jar + ``` + +## Command Examples + +* **Adding a candidate**: + ``` + add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 j/Software Engineer tm/IOS Development t/Swift + ``` + +* **Scheduling an interview**: + ``` + interview 1 2025-04-01 10:00 40 + ``` + +# Installation +## Prerequisites + +Java (JDK 17 zulu version) +Gradle + +## Steps + +### Clone the Repository + +``` +git clone https://github.com/AY2425S2-CS2103T-F14-3/tp.git +cd tp +``` +### Build the Project + +``` +gradle build # Using Gradle +``` + +### Run the Application +``` +java -jar RecruitIntel.jar # Running the JAR +``` + +# Acknowledgements +This project is based on the AddressBook-Level3 project created by +the [SE-EDU initiative](https://se-education.org). diff --git a/build.gradle b/build.gradle index 0db3743584e..66ab3f5980b 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,10 @@ repositories { maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } } +run { + enableAssertions = true +} + checkstyle { toolVersion = '10.2' } @@ -66,7 +70,7 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'RecruitIntel.jar' } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index ff3f04abd02..8a2944811a2 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,47 @@ title: About Us We are a team based in the [School of Computing, National University of Singapore](https://www.comp.nus.edu.sg). -You can reach us at the email `seer[at]comp.nus.edu.sg` +You can reach us at the email `e1300530@u.nus.edu` ## Project team -### John Doe +### Huang Zekai - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] - -* Role: Project Advisor - -### Jane Doe - - - -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[homepage](https://www.linkedin.com/in/zekai-huang-23213b28b)] +[[github](https://github.com/hzk-lab)] -* Role: Team Lead -* Responsibilities: UI +* Role: Scheduling and tracking, including defining, assigning, and tracking project tasks. +* Responsibilities: Data Structure + Threading -### Johnny Doe +### Benedict Bryan Tjandra - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/bryantjandra)] +[[portfolio](team/bryantjandra.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: UI + Code Quality + -### Jean Doe +### GONG HAOZHEN - + -[[github](http://github.com/johndoe)] +[[github](https://github.com/LsmnBmnc)] [[portfolio](team/johndoe.md)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Develop new features -### James Doe +### Li Daoxin - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/Prog-Neuro-Com)] +[[portfolio](team/prog-neuro-com)] * Role: Developer -* Responsibilities: UI +* Responsibilities: Dev Ops + Threading diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 743c65a49d2..1933c607818 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,14 +2,37 @@ layout: page title: Developer Guide --- -* Table of Contents -{:toc} +- [Acknowledgements](#acknowledgements) +- [Setting up, getting started](#setting-up-getting-started) +- [Design](#design) + - [Architecture](#architecture) + - [UI component](#ui-component) + - [Logic component](#logic-component) + - [Model component](#model-component) + - [Storage component](#storage-component) + - [Common classes](#common-classes) +- [Implementation](#implementation) + - [Proposed Undo/redo feature](#proposed-undoredo-feature) +- [Documentation, logging, testing, configuration, dev-ops](#documentation-logging-testing-configuration-dev-ops) +- [Appendix: Requirements](#appendix-requirements) + - [Product scope](#product-scope) + - [User stories](#user-stories) + - [Use cases](#use-cases) + - [Non-Functional Requirements](#non-functional-requirements) + - [Glossary](#glossary) +- [Appendix: Instructions for manual testing](#appendix-instructions-for-manual-testing) + - [Launch and shutdown](#launch-and-shutdown) + - [Deleting a person](#deleting-a-person) + - [Editing a person](#editing-a-persons-information) + - [Finding a person](#finding-a-person) -------------------------------------------------------------------------------------------------------------------- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* Code and idea for Undo and Redo were + inspired by the original AB3 Developer Guide. +* The `LocalDateTime` library was used for handling date and time functionalities. -------------------------------------------------------------------------------------------------------------------- @@ -36,7 +59,7 @@ Given below is a quick overview of main components and how they interact with ea **Main components of the architecture** -**`Main`** (consisting of classes [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. +**`Main`** (consisting of classes [`Main`](https://github.com/AY2425S2-CS2103T-F14-3/tp/blob/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2425S2-CS2103T-F14-3/tp/blob/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. * At app launch, it initializes the other components in the correct sequence, and connects them up with each other. * At shut down, it shuts down the other components and invokes cleanup methods where necessary. @@ -68,13 +91,13 @@ The sections below give more details of each component. ### UI component -The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) +The **API** of this component is specified in [`Ui.java`](https://github.com/AY2425S2-CS2103T-F14-3/tp/blob/master/src/main/java/seedu/address/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/AY2425S2-CS2103T-F14-3/tp/blob/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2425S2-CS2103T-F14-3/tp/blob/master/src/main/resources/view/MainWindow.fxml) The `UI` component, @@ -85,7 +108,7 @@ The `UI` component, ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2425S2-CS2103T-F14-3/tp/blob/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: @@ -115,14 +138,14 @@ How the parsing works: * All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) +**API** : [`Model.java`](https://github.com/AY2425S2-CS2103T-F14-3/tp/blob/master/src/main/java/seedu/address/model/Model.java) The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). +* stores the RecruitIntel data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). * stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) @@ -136,15 +159,30 @@ The `Model` component, ### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](https://github.com/AY2425S2-CS2103T-F14-3/tp/blob/master/src/main/java/seedu/address/storage/Storage.java) The `Storage` component, -* can save both address book data and user preference data in JSON format, and read them back into corresponding objects. +* can save both RecruitIntel 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" : "David Li", + "phone" : "91031282", + "email" : "lidavid@example.com", + "address" : "Blk 436 Serangoon Gardens Street 26, #16-43", + "jobPosition" : "Security Specialist", + "team" : "Apple Store", + "notes" : "Expert in iOS security protocols and penetration testing. Previously led security at a major tech firm. Identified critical vulnerabilities in App Store submission process. Strong security mindset. Published research on mobile app security. Excellent communicator when explaining complex security concepts.", + "tags" : [ "what", "shw" ], + "startTime" : "2025-01-01 13:45", + "duration" : "15" + } +``` ### Common classes Classes used by multiple components are in the `seedu.address.commons` package. @@ -155,37 +193,37 @@ 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 #### Proposed Implementation The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +* `VersionedAddressBook#commit()` — Saves the current RecruitIntel state in its history. +* `VersionedAddressBook#undo()` — Restores the previous RecruitIntel state from its history. +* `VersionedAddressBook#redo()` — Restores a previously undone RecruitIntel state from its history. These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial RecruitIntel state, and the `currentStatePointer` pointing to that single RecruitIntel 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. The user executes `delete 5` command to delete the 5th person in the RecruitIntel. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the RecruitIntel after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted RecruitIntel state. ![UndoRedoState1](images/UndoRedoState1.png) -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified RecruitIntel state to be saved into the `addressBookStateList`. ![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#commitAddressBook()`, so the RecruitIntel 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. 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 RecruitIntel state, and restores the RecruitIntel to that state. ![UndoRedoState3](images/UndoRedoState3.png) @@ -206,17 +244,17 @@ Similarly, how an undo operation goes through the `Model` component is shown bel ![UndoSequenceDiagram](images/UndoSequenceDiagram-Model.png) -The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the RecruitIntel to that state. -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest RecruitIntel state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.
-Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +Step 5. The user then decides to execute the command `list`. Commands that do not modify the RecruitIntel, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. ![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. +Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all RecruitIntel 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. ![UndoRedoState5](images/UndoRedoState5.png) @@ -228,7 +266,7 @@ The following activity diagram summarizes what happens when a user executes a ne **Aspect: How undo & redo executes:** -* **Alternative 1 (current choice):** Saves the entire address book. +* **Alternative 1 (current choice):** Saves the entire RecruitIntel. * Pros: Easy to implement. * Cons: May have performance issues in terms of memory usage. @@ -237,12 +275,6 @@ The following activity diagram summarizes what happens when a user executes a ne * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). * Cons: We must ensure that the implementation of each individual command are correct. -_{more aspects and alternatives to be added}_ - -### \[Proposed\] Data archiving - -_{Explain here how the data archiving feature will be implemented}_ - -------------------------------------------------------------------------------------------------------------------- @@ -262,121 +294,537 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +* Manages a high volume of candidates across multiple hiring roles. +* Prefers fast, efficient tools over traditional GUI-based applicant tracking systems (ATS). +* Comfortable using CLI-based applications and prefers keyboard-driven workflows. +* Needs quick access to candidate details, notes, and filtering functions for better decision-making. +* Requires an organized and structured way to track candidate interactions, evaluations, and hiring progress. + +**Value proposition**: + +A highly efficient, CLI-driven contact management system that enables Apple’s HR recruiters to quickly add, retrieve, and evaluate candidates, reducing manual screening time and improving hiring accuracy. -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | - -*{More to be added}* +| Priority | As a …​ | I want to …​ | So that I can…​ | +|----------|-----------|----------------------------------------------------------------|---------------------------------------------------------------------------------------| +| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App or when I start using the app. | +| `* * *` | recruiter | add a candidate’s name, contact details, and role applied for | keep track of them easily. | +| `* * *` | recruiter | list all candidates in the system | see at a glance who is currently in the database. | +| `* * *` | recruiter | remove outdated or irrelevant candidate records | maintain a clean list. | +| `* * *` | recruiter | find candidates by their names or skills | quickly locate specific individuals. | +| `* * *` | recruiter | record the candidate's interview performance | facilitate subsequent admission evaluation. | +| `* * *` | recruiter | add candidates' interview time | schedule an interview. | +| `* *` | recruiter | sort candidates by increasing interview time | know who is the first to be interviewed. | +| `* *` | recruiter | edit a candidate's details (e.g. phone, email) | correct mistakes and keep data accurate conveniently. | +| `* *` | recruiter | classify candidates by a combination of attributes | filter candidates based on specific criteria. | ### 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 `RecruitIntel` and the **Actor** is the `user`, unless specified otherwise) + -**Use case: Delete a person** +#### Use Case: UC01 - Add New Candidate **MSS** +1. HR Recruiter adds a new candidate. +2. **System** validates the details. +3. **System** creates a new candidate record and displays a success message. + Use case ends. -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 +**Extensions** + +- **1a.** HR Recruiter omits a mandatory detail. + - **1a1.** **System** shows an error message and requests correction. + - **1a2.** HR Recruiter enters the required detail. + - Steps 1a1–1a2 are repeated until all mandatory details are provided. + Use case resumes from step 2. + +- **2a.** **System** detects invalid data (e.g., phone is non-numeric or email is malformed). + - **2a1.** **System** shows an error message and requests correction. + - **2a2.** HR Recruiter enters valid data. + - Steps 2a1–2a2 are repeated until all data are valid. + Use case resumes from step 3. + +- **2b.** **System** detects a duplicate candidate (same email). + - **2b1.** **System** notifies the recruiter that the candidate already exists. + - Steps 2b1–2b2 are repeated until all data are valid. + Use case resumes from step 3. + + Use case ends. + +#### Use Case: UC02 - List All Candidate + +**MSS** +1. HR Recruiter chooses to list all candidates. +2. **System** retrieves all candidates. +3. **System** displays the list of candidates. + Use case ends. - Use case ends. +#### Use Case: UC03 - Edit Candidate Information + +**MSS** +1. HR Recruiter chooses to edit a candidate. +2. **System** requests the candidate to be edited. +3. HR Recruiter enters the candidate to be edited. +4. **System** requests the new details for the candidate. +5. HR Recruiter enters the new details for the candidate. +6. **System** updates the candidate’s details and display a success message. + +Use case ends. + +**Extension** +- **3a.** **System** detects an invalid candidate. + - **3a1.** **System** requests the recruiter to enter a valid candidate. + - **3a2.** HR Recruiter enters a new valid candidate. + - Steps 3a1–3a2 are repeated until the input is valid. + Use case resumes from step 4. +- **5a.** **System** detects an invalid detail. + - **5a1.** **System** requests the recruiter to enter a valid detail. + - **5a2.** HR Recruiter enters a new valid detail. + - Steps 5a1–5a2 are repeated until the input is valid. + Use case resumes from step 6. + +Use case ends. + +#### Use Case: UC04 - Classify Candidate +**MSS** +1. HR Recruiter chooses to classify some candidates based on tags. +2. **System** requests the tags to be used to classify. +3. **System** displays the candidates classified by tags. + +Use case ends. + +#### Use Case: UC05 - Find Candidate +**MSS** +1. HR Recruiter chooses to find a candidate. +2. **System** requests the keywords to be used to find candidate. +3. HR Recruiter enters the keywords to find. +4. **System** finds the candidate and displays the candidate. + + Use case ends. + +#### Use Case: UC06 - Enter Note for Candidate +**MSS** +1. HR Recruiter chooses to enter a note for a candidate. +2. **System** requests the candidate that to be added a note. +3. HR Recruiter enters the chosen candidate. +4. **System** requests the note to be added for the candidate. +5. HR Recruiter enters the note for the candidate. +6. **System** displays the note for the candidate. + Use case ends. + +**Extension** +- **3a.** **System** detects an invalid candidate. + - **3a1.** **System** requests the recruiter to enter a valid candidate. + - **3a2.** HR Recruiter enters a new valid candidate. + - Steps 3a1–3a2 are repeated until the input is valid. + Use case resumes from step 4. +- **5a.** **System** detects an invalid note. + - **5a1.** **System** requests the recruiter to enter a valid note. + - **5a2.** HR Recruiter enters a new valid note. + - Steps 5a1–5a2 are repeated until the input is valid. + Use case resumes from step 6. +Use case ends. + +#### Use Case: UC07 - Delete Candidate + +**MSS** +1. HR Recruiter chooses to delete a candidate. +2. **System** requests the candidate to be deleted. +3. HR Recruiter enters the candidate to be deleted. +4. **System** deletes the candidate and displays a success message. + +Use case ends. **Extensions** +- **3a.** **System** detects an invalid candidate. + - **3a1.** **System** requests the recruiter to enter a valid candidate. + - **3a2.** HR Recruiter enters a new valid candidate. + - Steps 3a1–3a2 are repeated until the input is valid. + Use case resumes from step 4. -* 2a. The list is empty. +Use case ends. - Use case ends. +#### Use Case: UC08 - Add Interview Time Information for Candidate +**MSS** +1. HR Recruiter chooses to add interview time information for a candidate. +2. **System** requests the candidate to be added interview time information. +3. HR Recruiter enters the candidate to be added interview time information. +4. **System** requests the interview time information for the candidate. +5. HR Recruiter enters the interview time information for the candidate. +6. **System** displays the interview time information for the candidate. + Use case ends. -* 3a. The given index is invalid. +**Extensions** +- **3a.** **System** detects an invalid candidate. + - **3a1.** **System** requests the recruiter to enter a valid candidate. + - **3a2.** HR Recruiter enters a new valid candidate. + - Steps 3a1–3a2 are repeated until the input is valid. + Use case resumes from step 4. +- **5a.** **System** detects an invalid interview time information. + - **5a1.** **System** requests the recruiter to enter a valid interview time information. + - **5a2.** HR Recruiter enters a new valid interview time information. + - Steps 5a1–5a2 are repeated until the input is valid. + Use case resumes from step 6. + +Use case ends. - * 3a1. AddressBook shows an error message. +#### Use Case: UC09 - Sort Candidates + +**MSS** +1. HR Recruiter chooses to sort candidates. +2. **System** sorts the candidates based on the interview time. +3. **System** displays the sorted list. + Use case ends. - Use case resumes at step 2. +#### Use Case: UC10 - Undo Actions +**MSS** +1. HR Recruiter chooses to undo the last action. +2. **System** undoes the last action. + Use case ends. + +**Extensions** +- **1a.** **System** detects that there are no actions to undo. + - **1a1.** **System** shows an error message and requests the recruiter to enter a valid action. -*{More to be added}* +Use case ends. + +#### Use Case: UC11 - Redo Actions +**MSS** +1. HR Recruiter chooses to redo the last action. +2. **System** redoes the last action. + Use case ends. + +**Extensions** +- **1a.** **System** detects that there are no actions to redo. + - **1a1.** **System** shows an error message and requests the recruiter to enter a valid action. + +Use case ends. + +#### Use Case: UC12 - Clear All Candidates +**MSS** +1. HR Recruiter chooses to clear all candidates. +2. **System** clears all candidates. + 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. - -*{More to be added}* +4. Should ensure that all data entries are processed within 5 seconds under normal operating conditions, providing quick feedback to user interactions. +5. Application should maintain user data privacy and security, adhering to the latest data protection regulations. +6. The system should be user-friendly, with an interface that requires no more than 20 minutes of training for new users to perform basic operations. +7. The system should store candidate information securely. +8. The system should not crash or freeze when an invalid command is entered. +9. The system should be self-contained and should not require an internet connection or external databases. ### Glossary - -* **Mainstream OS**: Windows, Linux, Unix, MacOS -* **Private contact detail**: A contact detail that is not meant to be shared with others - +* **AB3**: The codebase provided as a starting point for this project. RecruitIntel extends and modifies AB3. +* **CLI (Command Line Interface)**: A user interface navigated by typing commands into a terminal or console window. +* **GUI (Graphical User Interface)**: An interface allowing users to interact with electronic devices through graphical icons and visual indicators. +* **JSON (JavaScript Object Notation)**: A lightweight data-interchange format, easy for humans to read and write, and easy for machines to parse and generate. +* **Mainstream OS**: Operating systems that are widely used and supported, such as Windows, macOS, Linux, and UNIX. +* **Parser**: A component that interprets text data within a file according to predefined rules or specifications. +* **PlantUML**: A tool for quickly writing and sharing visual representations of programs, algorithms, and systems. +* **Sequence Diagram**: A type of UML diagram showing how objects operate with one another and in what order. +* **State Pointer**: A reference point tracking the current state or position in a sequence of states. +* **UML (Unified Modeling Language)**: A standardized modeling language consisting of an integrated set of diagrams. +* **XML (eXtensible Markup Language)**: A markup language defining rules for encoding documents in a format that is both human-readable and machine-readable. +* **Tag**: A keyword or term assigned to a piece of information, making it easier to search for and locate. +* **Candidate**: A person who applies for a job or position, often undergoing an interview process. +* **Interview**: A formal meeting in which a candidate is evaluated for a position. +* **Recruiter**: A person responsible for finding and hiring candidates for job positions. -------------------------------------------------------------------------------------------------------------------- ## **Appendix: Instructions for manual testing** Given below are instructions to test the app manually. -
:information_source: **Note:** These instructions only provide a starting point for testers to work on; -testers are expected to do more *exploratory* testing. - -
- ### Launch and shutdown 1. Initial launch 1. Download the jar file and copy into an empty folder - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 2. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. -1. Saving window preferences +2. Saving window preferences 1. Resize the window to an optimum size. Move the window to a different location. Close the window. - 1. Re-launch the app by double-clicking the jar file.
+ 2. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ +3. Exit the App + +Enter exit in the command box. This will exit the app. + +### Adding a person + +Adding a person in the person list: + + 1. Test case `add n/Byran high p/33665544 e/byranh@example.com a/123, Clementi Rd, 335544 j/Hardware Engineer tm/Machine Learning System t/C t/AI`
+ Assumption: We assume there is no person with duplicate email.
+ Excepted:Person added successfully and displayed in the list. + 2. Test Case (duplicate email): `add n/Anna Doe p/12345678 e/byranh@example.com a/789, Pasir Panjang Rd, 112233 j/Software Engineer tm/Frontend`
+ Assumption: We assume there is a person with duplicate email in the list.
+ Excepted:Error message shown indicating email duplication. + 3. Test Case : `add n/Anna Doe e/bryanh@ab.com a/789, Pasir Panjang Rd, 112233 j/Software Engineer `
+ Excepted:No person is added. Error message shown indicating wrong format. ### Deleting a person -1. Deleting a person while all persons are being shown +Deleting a person while all persons are being shown: 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. - 1. Test case: `delete 1`
+ 2. Test case: `delete 1`
Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. - 1. Test case: `delete 0`
+ 3. Test case: `delete 0`
Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ 4. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
Expected: Similar to previous. -1. _{ more test cases …​ }_ - -### Saving data - -1. Dealing with missing/corrupted data files - - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ +### Editing a person's information +Editing a person in the person list: + 1. Assumption: We assume the index here is valid if it is a positive integer
+ Test case: `edit 1 t/Java`
+ Expected: The Tag of the first person in the list is changed to Java + 2. Test case: `edit 0 t/Java`
+ Expected: No person is edited, an error message will be displayed. + 3. Other invalid edit command to try :
+ `edit -1 p/00`
+ `edit 1 e/ab@cd`
+ Expected: No person is edited, an error message will be displayed. + + 4. Other valid edit command to try :
+ `edit 1 n/Robin gen`
+ `edit 1 p/96754328`
+ Expected: the field indicated by the prefix is edit to the information given. + +### Finding a person +Finding a person by name: + 1. Test case: `find alice`
+ Expected: All persons with the name that contains `alice` are shown in the list. Other persons are hidden. + +### Classifying Persons +Classifying Persons by some of the tags, team and jobPosition: + 1. Test case: `classify t/Python tm/Design j/Software Engineer`
+ Expected: Displays persons matching all given filters. + + 2. Test case : `classify t/Python`
+ Expected: Displays persons have tag Python. + +### Clearing All Persons +Clearing All Persons in the list: +1. Test Case: `clear`
Expected: All entries are deleted. Application shows an empty list. + +### Scheduling an Interview +Scheduling an interview with duration for a person with positive integer indexes: +1. Assumption: The minutes of StartTime and Duration must be multiple of 5. +2. Test case: `interview 1 2025-04-01 10:00 40`
+ Expected: Interview scheduled with startTime 2025-04-01 10:00 and Duration 40; +3. Test case: `interview 0 2025-04-12 14:00 20`
+ Expected: No interview is scheduled, an error message will be displayed. +4. Other invalid interview command to try:
+ `interview 1 2025-04-12 14:00 23`
+ `interview 1 2025-04-12 14:12 20`
+ `interview 1 2025 04-12 14:12 15`
+ Expected: No interview is scheduled, an error message will be displayed. + +### Adding a Note +Adding a note for a person: +1. Assumption: The index here is valid if it is a positive integer
+2. Test case: `note 1 Excellent technical skills observed during the interview.`
+ Expected: Note successfully added to the person's details. + +### Sorting the List +Sort the persons by interview startTime in the current list by ascending order: +1. Test case: `sort`
+ Expected: List sorted by person's interview time. For persons without interviews,they are displayed at the end of the list. + +### Viewing Help +Viewing help for the commands: +1. Test case: `help`
+ Expected: Help window opens showing the link of User Guide. + +### Undo +Restores the previous state: +1. Assumption: Last command belongs to modifying commands (add, delete, edit, etc.) +2. Test case: undo
+ Expected: Last operation is reversed. The person list is restored to the previous state. + +### Redo +Reapplies the last undone action: +1. Assumption: Last command is undo. +2. Test case: redo
+ Expected: Previously undone action reapplied. The person list information is changed. + + +## **Planned Enhancements** + +Team size: 4 +1. **Support Multiple Job Applications per Candidate**: Currently, candidates can only apply for one job position and one team at a time, which doesn't reflect real-world scenarios where candidates might be able to apply for multiple roles. We plan to enhance this by: + * Modifying the `add` command to accept multiple job positions and teams: + ``` + add n/NAME p/PHONE e/EMAIL a/ADDRESS j/[JOB_POSITION_1, JOB_POSITION_2] + tm/[TEAM_1, TEAM_2] [t/TAG]... + ``` + * Example usage: + ``` + add n/John Doe p/98765432 e/johnd@example.com a/123 Apple Park Way + j/[Software Engineer, Product Manager] tm/[iOS, Product] t/Swift + t/UIKit t/Agile + ``` + * Updating the UI to display multiple roles in the candidate card + * Enhancing the `classify` command to match any of the candidate's roles: + ``` + classify j/Software Engineer + // Will show candidates who applied for Software Engineer, + // even if they applied for other roles too + ``` + * Modifying the storage format to support arrays of job positions and teams per candidate + +2. **Prevent Scheduling Interviews in the Past**: Currently, RecruitIntel allows scheduling interviews for past dates, which can lead to confusion and invalid data. We plan to enhance the interview scheduling feature by: + * Adding date validation to ensure interview times are always in the future: + ``` + // Current behavior + interview 1 2023-01-01 10:00 30 // Allows scheduling in the past + + // Enhanced behavior + interview 1 2023-01-01 10:00 30 + // Error: Cannot schedule interviews in the past. The interview date + // must be after the current date and time + ``` + * Implementing real-time date validation that: + - Checks if the proposed interview date/time is at least 1 hour in the future + - Provides helpful error messages with the current date/time for reference + +3. **Enhanced Interview Sorting with Past Interview Handling**: Currently, the sort command doesn't distinguish between upcoming and past interviews, making it difficult to figure out if an interview has happened or not. We plan to enhance the sorting functionality by: + * Adding flags to the sort command for more flexible sorting options: + ``` + //Default: sorts all interviews + sort + + //Sorts interviews that haven't been completed first + sort --nc + ``` + * Key behaviors: + - sort --nc sorts upcoming interviews and places past interviews at the end, right before candidates with no interviews + - Adds visual indicators for past interviews in the UI + +4. **Improved Tag Management**: Currently, tags are completely replaced when editing, which can lead to accidental tag deletion. We plan to enhance tag management by: + * Adding specific tag commands: + ``` + tag 1 add t/Python // Adds Python tag to candidate #1 + tag 1 remove t/Python // Removes only Python tag + tag 1 clear // Removes all tags + ``` + * Example usage scenarios: + ``` + // Current behavior (using edit command) + edit 1 t/Python // Replaces ALL existing tags with just Python + + // Enhanced behavior (using new tag commands) + tag 1 add t/Python // Adds Python while preserving existing tags + tag 1 remove t/Java // Removes only Java tag, keeps others + ``` + * Implementing tag validation: + - Prevents duplicate tags automatically + - Warns before removing all tags + * Adding tag management features: + - Group related tags (e.g., programming languages, soft skills) + +5. **Enhanced Interview Command with Types**: Currently, the interview command only captures timing information without structuring the recruitment process. We plan to enhance the interview scheduling system by: + * Adding interview type for better tracking of candidates in the recruitment process: + ``` + //Schedule HR Interview + interview 1 2024-03-20 14:00 60 --ty HR + + //Schedule 1st Technical Interview + interview 1 2024-03-22 10:00 90 --ty TECH --n 1 + + //Schedule Hiring Manager Interview + interview 1 2024-03-25 15:00 45 --ty HM + ``` + * Supporting standard interview types: + - HR: Initial screening interview + - TECH: Technical assessment (can be numbered 1,2,...) + - HM: Hiring manager interview + - TEAM: Team fit discussion + - SYS: System design interview + - CODE: Coding assessment + - BEHAV: Behavioral interview + - FIN: Final round interview + + * Managing interview status: + ``` + // Removes the scheduled interview for candidate #1 + interview 1 clear + ``` + * Adding type-based filtering: + ``` + list interview --ty TECH // List candidates with technical interviews + ``` + +6. **Enhanced Notes Management**: Currently, notes can only be added as a single entry with limited character space. We plan to enhance the notes functionality by: + * Adding commands for more flexible note management: + ``` + // Adds a note entry for candidate #1 + note 1 add Great technical skills in the system design interview + + // Appends some text to the 1st note + note 1 append 1 Also showed strong team collaboration mindset + + // Clears all text of the 1st note + note 1 clear 1 + + // Removes the second note + note 1 remove 2 + ``` + * Supporting enhanced note features: + - Increased character limit to 1500 characters for each note + - Multiple note entries for each candidate + - Timestamp for each note entry + * Example usage: + ``` + > note 1 add Technical Interview: Candidate showed strong Java knowledge. + Added note #1 to John Doe + + > note 1 append Particularly impressed with his system design skills. + Updated note #1 for John Doe + + > note 1 add Strong communication and intrapersonal skills. Involved + with alot of social work and clubs at university. + Added note #2 to John Doe + + > note 1 list + Notes for John Doe: + 1. Technical Interview: Candidate showed strong Java knowledge. + Particularly impressed with his system design skills. + 2. Strong communication and intrapersonal skills. Involved with alot + of social work and clubs at university. + ``` + +7. **Enhanced Undo Redo Messages**: currently executing the undo and redo command will only show that undo or redo is successful. To help the user better know what has been undone or redone, we plan to enhance the undo and redo command by: + * Adding messages to show what action was undone or redone. + * Example usage: + ``` + > undo + Added Candidate: Bernice Yu; Phone: 99272758; Email: berniceyu@example.com; Address: Blk 30 Lorong 3 Serangoon Gardens, #07-18; Job Position: UI/UX Designer; Team: Design; Tags: [figma][design][experienced] + Undo successful! + + > redo + Deleted Candidate: Bernice Yu; Phone: 99272758; Email: berniceyu@example.com; Address: Blk 30 Lorong 3 Serangoon Gardens, #07-18; Job Position: UI/UX Designer; Team: Design; Tags: [figma][design][experienced] + Redo successful! + ``` + -1. _{ more test cases …​ }_ diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 27c2d1cf16c..c526ec085d8 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,7 +3,29 @@ 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. +RecruitIntel is a **desktop application designed specifically for Apple's HR recruiters** to efficiently manage candidate information. While offering an easy-to-use visual interface, it features powerful text commands that help process candidate information considerably faster than traditional mouse-based applications. +### Who is RecruitIntel For? +**Primary Users:** +* HR recruiters at Apple, particularly those handling: + * Hardware teams (chip design, hardware engineering) + * Software teams (iOS, macOS, website development) + * Operations teams (retail, security, facilities) + +**Prerequisites:** +* Basic familiarity with command-line interfaces +* Experience with candidate management systems +* Access to a computer with Java 17 or above + +### What RecruitIntel Does +**Core Features:** +* 🚀 Fast candidate information entry and search +* 🏷️ Classification system for candidate organization +* 📅 Interview scheduling and management +* 📝 Note-taking capability for interview feedback +* 💾 Automated saving of data after changes
+ +
+Navigate through sections using the table of contents below: * Table of Contents {:toc} @@ -12,31 +34,42 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo ## Quick start -1. Ensure you have Java `17` or above installed in your Computer.
+1. Ensure you have Java `17` or above installed in your computer.
**Mac users:** Ensure you have the precise JDK version prescribed [here](https://se-education.org/guides/tutorials/javaInstallationMac.html). -1. Download the latest `.jar` file from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Download the latest `RecruitIntel.jar` from our [releases page](https://github.com/AY2425S2-CS2103T-F14-3/tp/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +3. Create a dedicated folder for RecruitIntel: + * Choose a location with at least 1GB of free space + * Ensure you have write permissions for this location + * Copy the downloaded JAR file into this folder + +4. Launch RecruitIntel: + * Open your terminal/command prompt + * Navigate to the folder containing the JAR file: `cd path/to/folder` + * Run the application: `java -jar recruitintel.jar` + * A screen similar to the one below should appear within 5 seconds: -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.
+ +5. Type the command in the command box and press Enter to execute it. For example, typing **`help`** and pressing Enter will open the help window.
Some example commands you can try: - * `list` : Lists all contacts. + * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 j/Software Engineer tm/IOS Development` : Adds a candidate named `John Doe` applying for a `Software Engineer` position in the `IOS Development` team. + + * `note 1 Strong backend experience, but lacks iOS exposure.` : Adds a note to the 1st candidate in the list. + + * `interview 1 2025-04-01 10:00 40` : Schedules a 40-minute interview for the 1st candidate at 10:00 AM on April 1, 2025. - * `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. + * `sort` : Sorts the candidates by ascending order of their scheduled interview time. - * `delete 3` : Deletes the 3rd contact shown in the current list. + * `undo` : Reverts the most recent change (e.g. adding, deleting, or editing a candidate). - * `clear` : Deletes all contacts. + * `clear` : Deletes all candidates. - * `exit` : Exits the app. -1. Refer to the [Features](#features) below for details of each command. +6. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- @@ -46,155 +79,545 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo **:information_source: Notes about the command format:**
-* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. +* Replace words in `UPPER_CASE` with the required information. + * Example: in `add n/NAME`, replace `NAME` with the actual name like `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 in square brackets `[...]` are optional. + * Example: `[t/TAG]` can be used as `t/Python` or omitted entirely -* Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. +* Items with `…`​ can be used multiple times or omitted. + * Example: `[t/TAG]…​` allows: + * No tags: ` ` + * One tag: `t/Python` + * Multiple tags: `t/Python t/Java t/AWS` -* Parameters can be in any order.
+* You can enter the information in any order.
e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
- 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. -
- -### Viewing help : `help` +* The `sort` command only sorts **currently displayed** candidates (i.e. those shown by the last command such as `classify`, `find`, etc.). -Shows a message explaning how to access the help page. +* When using `classify` to filter candidates, it will show only candidates that match all of your search criteria. + Example: `classify t/python tm/Design` shows candidates who have both Python skills and applied for a position in the Design team. -![help message](images/helpMessage.png) +* For simple commands like `help`, `list`, `exit`, and `clear`, any extra information you type will be ignored. + Example: typing `help 123` works the same as typing just `help`. -Format: `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. +
-### Adding a person: `add` +### Viewing help : `help` -Adds a person to the address book. +Opens a window with a link to RecruitIntel's complete user guide + +**Format**: +``` +help +``` + +**Key behaviors**: +* Window can be resized and moved + +Expected output:
+![result for help message command'](images/helpMessage.png) + +### Adding a candidate: `add` + +Adds a new candidate to RecruitIntel with their details and applied position. + +**Format**: +``` +add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS j/JOB_POSITION_APPLIED +tm/TEAM_APPLIED [t/TAG]…​ +``` + +**Key behaviors**: +* All fields except tags are mandatory +* A candidate is considered a duplicate if they share the same email as an existing candidate. +* Email must follow these rules: + - Before @: Must start/end with letters/numbers and can contain (+, ., -) in between + - After @: Must contain at least two parts (e.g., example.com) and each part can only use letters, numbers, and hyphens + - Case sensitivity: Emails are case-insensitive (e.g., John@EXAMPLE.com equals john@example.com) +* Each candidate can only apply to one job position and one team at a time + +**Examples**: +* Adding an iOS developer: + ``` + add n/John Doe p/98765432 e/johnd@example.com a/123 Apple Park Way + j/Software Engineer tm/iOS Development t/Swift t/UIKit + ``` +* Adding a chip designer: + ``` + add n/Jane Smith p/91234567 e/janes@example.com a/456 Infinite Loop + j/Hardware Engineer tm/Chip Design t/Verilog t/ASIC + ``` + +💡 **Tips**: +* Use meaningful tags to track candidate skills and experiences +* Add multiple tags to make classification easier later on + +⚠️ **Warning**: +* Tags cannot contain spaces - use hyphens instead if needed (e.g., use machine-learning and not machine learning) + +Expected output: +``` +New candidate added: John Doe; Phone: 98765432; Email: johnd@example.com; +Address: 123 Apple Park Way; Job Position: Software Engineer; +Team: iOS Development; Tags: [UIKit][Swift] +``` + +### Listing all candidates : `list` + +Shows a complete list of all candidates in RecruitIntel. + +**Format**: +``` +list +``` + +**Key behaviors**: +* Shows all candidates regardless of previous filters +* Displays candidates in order of when they were added + +💡 **Tips**: +* Use this to reset the view of candidates after multiple rounds of filtering +* Helpful for getting correct indexes before using commands like `edit` or `delete` + +Expected output: +``` +Listed all candidates +``` + +### Editing a candidate : `edit` + +Modifies existing candidate information in RecruitIntel. + +**Format**: +``` +edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [j/JOB_POSITION_APPLIED] + [tm/TEAM_APPLIED] [t/TAG]…​ +``` + +**Key behaviors**: +* `INDEX` must be a valid positive integer from the currently displayed list and not greater than the total number of candidates shown +* At least one field must be provided for editing + +**Examples**: +* Update contact information: + ``` + edit 1 p/91234567 e/johndoe@example.com + ``` +* Update job position applied, team applied, and tags: + ``` + edit 2 j/Machine Learning Engineer tm/AI Development t/Python t/TensorFlow + ``` + +💡 **Tips**: +* Use `list` to see all candidates and their indexes first +* An empty tag field (`t/`) removes all tags for that candidate + +⚠️ **Warning**: +* Changes cannot be partially undone +* All existing tags are replaced when editing tags + +Expected output: +``` +Edited Candidate: Bryan Tjandra; Phone: 91234567; Email: johndoe@example.com; +Address: Blk 30 Geylang Street 29, #06-40; Job Position: Software Engineer; +Team: iOS Development; Tags: [senior][mobile][swift] +``` + +### Classifying candidates by attributes: `classify` + +Groups and displays candidates based on their tags, teams, and/or job positions. This helps quickly find candidates matching a specific criteria. + +**Format**: +``` +classify [t/TAG]... [tm/TEAM] [j/JOB_POSITION] +``` + +**Key behaviors**: +* Case-insensitive matching (e.g., `python` matches `Python`) +* Partial matching supported (e.g., `eng` matches `Engineer`) +* Shows candidates that match **all** provided criteria +* Only one team (`tm/`) and one job position (`j/`) are allowed +* Multiple tags (`t/`) are allowed - candidates must match all specified tags + +**Examples**: +* Find candidates with both frontend and React skills, applying for Software Engineer positions in the Web Development team: + ``` + classify t/frontend t/react j/Software Engineer tm/Web Development + ``` + +💡 **Tips**: +* Start with broader criteria and narrow down gradually +* Combine with `note` command to add observations about grouped candidates +* Regular classification helps identify skill patterns in your candidate pool + +⚠️ **Warning**: +* Multiple job positions or teams in one command are not allowed +* Empty results might mean your criteria are too restrictive +* Each tag needs its own `t/` prefix - do not combine multiple tags under one prefix + +Expected output: +![result for 'classify t/mobile j/Software Engineer tm/iOS Development'](images/ClassifyScreenshot.png) + +### Finding candidates by name: `find` + +Searches for candidates whose names contain any of the given keywords. + +**Format**: +``` +find KEYWORD [MORE_KEYWORDS] +``` + +**Key behaviors**: +* Case-insensitive search (e.g., `hans` matches `Hans`) +* Order of keywords doesn't matter, e.g. `Hans Bo` will match `Bo Hans` +* Partial word matching supported, e.g. `Han` will match `Hans` +* Candidates matching at least one keyword will be returned e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` + +**Examples**: +* Search by a single name: + ``` + find George + ``` + +💡 **Tips**: +* Consider common name variations (e.g., "Rob" for "Robert") when searching through a large pool of candidates + +Expected output: +![result for 'find George'](images/FindScreenshot.png) + +### Adding notes to a candidate: `note` + +Adds or updates interviewer notes for a candidate. + +**Format**: +``` +note INDEX NOTE_TEXT +``` + +**Key behaviors**: +* `INDEX` must be a valid positive integer from the currently displayed list and not greater than the total number of candidates shown +* Note text has a maximum of 450 characters +* New notes overwrites any existing notes for that candidate + +**Examples**: +* Add interview observations: + ``` + note 3 Great system design skills, good cultural fit. + Team lead interview recommended. + ``` + +💡 **Tips**: +* Be specific and objective in notes +* Include key technical observations + +⚠️ **Warning**: +* Cannot recover overwritten notes +* Stay within the character limit + +Expected output: +``` +Note added to candidate #3: "Great system design skills, good cultural fit. +Team lead interview recommended." +``` + +### Deleting a candidate : `delete` + +Removes a candidate from RecruitIntel permanently. + +**Format**: +``` +delete INDEX +``` + +**Key behaviors**: +* Deletion is permanent (but can be undone using the undo command) + +**Examples**: +* Delete after listing: + ``` + list + delete 2 + ``` + +💡 **Tips**: +* Use `undo` if needed +* Check whether the index is valid in the current view of candidates + +⚠️ **Warning**: +* Action cannot be reversed after closing app +* Verify index in filtered lists + +Expected output: +``` +Deleted Candidate: John Doe; Phone: 98765432; Email: johnd@example.com; +Address: 123 Apple Park Way; Job Position: Software Engineer; +Team: iOS Development; Tags: [UIKit][Swift] +``` + +### Scheduling an interview: `interview` + +Sets or updates interview times for a candidate. + +**Format**: +``` +interview INDEX START_TIME DURATION +``` + +**Key behaviors**: +* `INDEX` must be a valid positive integer from the currently displayed list and not greater than the total number of candidates shown +* START_TIME format: yyyy-MM-dd HH:mm - uses the 24-hour time format +* DURATION must be a multiple of 5 and cannot be over 1440 +* Interviews happen sequentially - Each candidate can only have one interview scheduled at a time +* New interview times will replace existing ones +* Different candidates can have interviews scheduled at the same time + +**Examples**: +* Schedule a morning interview for 40 minutes: + ``` + interview 1 2025-04-01 10:00 40 + ``` + +💡 **Tips**: +* Use `sort` to see the schedule for interview times +* Consider time zones for remote interviews +* Standard duration examples: + - 30 mins: Initial screening + - 45 mins: Technical assessment + - 60 mins: Team interviews + +⚠️ **Warning**: +* Verify date format carefully +* Double-check the index when scheduling interviews in filtered views + +Expected output: +``` +Interview set for candidate #1: Start = 2025-04-01 10:00, Duration = 40 minutes +``` + +### Sorting candidates by interview time: `sort` + +Sorts the currently displayed candidates by their scheduled interview start times in ascending order. + +**Format**: +``` +sort +``` + +**Key behaviors**: +* Only sorts the **current list** of displayed candidates +* Candidates with interviews are sorted by start time (ascending) +* Candidates without interviews appear at the end + +**Examples**: +* View interviews for the iOS Development team in order: + ``` + classify tm/iOS Development + sort + ``` + +💡 **Tips**: +* Sorting after modifying or adding candidates helps maintain a clear view of when candidates have interviews. + +Expected output: +``` +Sorted all candidates by their Interviews' Start Time. +``` + +### Undoing changes: `undo` + +Reverts RecruitIntel to its state before the last modifying command. + +**Format**: +``` +undo +``` + +**Key behaviors**: +* Restores the previous state +* Works with modifying commands, which includes `add`, `clear`, `delete`, `edit`, `interview` and `note`. +* Multiple `undo` commands will revert multiple changes + +**Examples**: +* Undo an accidental deletion: + ``` + delete 3 // Oops, wrong candidate! + undo // Candidate is restored + ``` + +💡 **Tips**: +* Use immediately after realizing a mistake -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +⚠️ **Warning**: +* Cannot undo after closing the application + +### Redoing changes: `redo` -
:bulb: **Tip:** -A person can have any number of tags (including 0) -
+Restores a previously undone command in RecruitIntel. -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` +**Format**: +``` +redo +``` + +**Key behaviors**: +* Restores the most recently undone change +* Only works after an `undo` command +* Multiple `redo` commands will restore multiple changes in sequence + +**Examples**: +* Redo an undone deletion: + ``` + delete 3 // Delete candidate + undo // Oops, needed that deletion + redo // Deletion restored + ``` -### Listing all persons : `list` +💡 **Tips**: +* Use to restore changes if you undo too many times +* Check the candidate list after redoing to verify changes -Shows a list of all persons in the address book. +⚠️ **Warning**: +* Cannot redo after closing the application -Format: `list` -### Editing a person : `edit` +### Clearing all data : `clear` -Edits an existing person in the address book. +Removes all candidate data from RecruitIntel. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +**Format**: +``` +clear +``` -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ -* At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +### Exiting the program : `exit` -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. +Closes RecruitIntel and saves all data. -### Locating persons by name: `find` +**Format**: +``` +exit +``` -Finds persons whose names contain any of the given keywords. +### Data Management -Format: `find KEYWORD [MORE_KEYWORDS]` +RecruitIntel handles your data with care and provides several ways to manage data. -* The search is case-insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +#### Automatic Saving +* All changes are saved immediately +* No manual save required +* Data remains after exiting the application -Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +#### Data File +* Location: `[JAR file location]/data/recruitintel.json` +* Format: JSON (human-readable) +* Can be manually edited (advanced users) -### Deleting a person : `delete` +
:exclamation: **Caution:** +If your changes to the data file makes its format invalid, RecruitIntel 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 RecruitIntel 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. +
-Deletes the specified person from the address book. -Format: `delete INDEX` +-------------------------------------------------------------------------------------------------------------------- -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index **must be a positive integer** 1, 2, 3, …​ +## FAQ -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. +**Q**: How do I transfer my candidate data to another computer?
+**A**: Follow these steps:
+1. Install RecruitIntel on the new computer +2. Locate the data file (`recruitintel.json`) on your old computer +3. Copy this file to the same location on the new computer +4. Start RecruitIntel on the new computer -### Clearing all entries : `clear` +**Q**: What should I do if the application won't start?
+**A**: Try these solutions:
+1. Verify Java 17 is installed correctly +2. Check the data file for corruption +3. Delete `preferences.json` if it exists +4. Contact support if issues persist: `recruitintel@gmail.com` -Clears all entries from the address book. +**Q**: Why does my `classify` command return no results?
+**A**: This could happen for several reasons:
+1. Your search criteria might be too specific, try removing some filters +2. Typos in the tags, team names, or job positions -Format: `clear` +**Q**: Why can't I schedule an interview for a specific time?
+**A**: Check these potential issues:
+1. Ensure the date format is correct (YYYY-MM-DD) +2. Verify the time is in 24-hour format +3. Make sure the duration is a multiple of 5 minutes -### Exiting the program : `exit` +## Known Issues -Exits the program. +1. **Multiple Screen Display Issue** + * **Problem**: Screen may open off-screen when switching from multiple to single screen + * **Solution**: Delete `preferences.json` and run the application again + * **Prevention**: Close application before changing display setup -Format: `exit` +2. **Help Window Behavior** + * **Problem**: Minimized help window doesn't respond to help command. The help window remains minimized + * **Solution**: Manually restore the minimized Help Window + * **Workaround**: Close and reopen application -### Saving the data +3. **Complex Command Formatting** + * **Problem**: Commands with multiple tags or long addresses are difficult to format correctly + * **Solution**: Break down complex commands into smaller steps + * **Example**: Instead of adding all tags at once, use `edit` to add tags gradually + * **Prevention**: Use the `help` command to verify command format before execution -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +4. **Data File Corruption** + * **Problem**: Application fails to start or behaves unexpectedly after manual data file edits + * **Solution**: Restore from backup or start with a fresh data file + * **Prevention**: Always create backups before manual edits + * **Workaround**: Use the application's commands instead of manual file edits -### Editing the data file +5. **Search Performance with Large Pool of Candidates** + * **Problem**: Slow search time when searching through extensive candidate lists + * **Solution**: Use more specific search criteria and combine the `classify` command with the `find` command + * **Prevention**: Archive or remove outdated candidates regularly + * **Tip**: Start with broader searches and refine gradually -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. +## Glossary -
: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. -
+The following terms are used throughout this guide: -### Archiving data files `[coming in v2.0]` +* **Index**: A numerical identifier assigned to each candidate in the displayed list. When executing commands that require candidate selection (e.g., `delete 1`), use the number corresponding to the candidate's position in the current view. `delete 1` will delete the 1st candidate shown in the displayed list. -_Details coming soon ..._ +* **Command**: A specific instruction entered into RecruitIntel to perform an action. Commands follow a structured format and are executed by pressing Enter. For example, the `add` command initiates the process of creating a new candidate record. --------------------------------------------------------------------------------------------------------------------- +* **Tag**: A label used to categorize and organize candidates based on specific attributes. Tags can represent skills, qualifications, or other relevant characteristics. For instance, `t/Swift` indicates proficiency in the Swift programming language. -## FAQ +* **Command Format**: The specific structure required for entering commands, including prefixes (e.g., `n/` for name) and separators. This format ensures accurate interpretation of user input. -**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. +* **JSON file**: A type of computer file that stores candidate information. --------------------------------------------------------------------------------------------------------------------- +* **Terminal/Command Prompt**: A program where you type commands to run RecruitIntel. It is a text-based way to start and control the application. -## Known issues +* **Field**: A specific piece of information about a candidate, such as their name, phone number, or email address. Fields are the individual components that make up a candidate's record and are identified by prefixes (e.g., `n/` for name, `p/` for phone number). -1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. -2. **If you minimize the Help Window** and then run the `help` command (or use the `Help` menu, or the keyboard shortcut `F1`) again, the original Help Window will remain minimized, and no new Help Window will appear. The remedy is to manually restore the minimized Help Window. +* **State**: The current condition or status of the application, including all candidate data, display settings, and any active filters. When you use commands like `undo` or `redo`, you are moving between different states of the application. --------------------------------------------------------------------------------------------------------------------- -## Command summary +## Command Summary Action | Format, Examples --------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` +**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS j/JOB_POSITION_APPLIED tm/TEAM_APPLIED [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 j/Staff Engineer tm/AI & Machine Learning t/Python t/AI` +**Classify** | `classify [t/TAG] [tm/TEAM] [j/JOB_POSITION]`
e.g., `classify t/python tm/Design j/Software Engineer` **Clear** | `clear` **Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` +**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [j/JOB_POSITION_APPLIED] [tm/TEAM_APPLIED] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com j/Data Scientist` +**Exit** | `exit` **Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` **Help** | `help` +**List** | `list` +**Interview** | `interview INDEX START_TIME DURATION`
e.g., `interview 1 2025-04-01 10:00 40` +**Note** | `note INDEX NOTE_TEXT`
e.g., `note 1 The interviewee really exceeded our expectations!` +**Sort** | `sort` +**Undo** | `undo` +**Redo** | `redo` + diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..9018742259e 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "RecruitIntel" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2425S2-CS2103T-F14-3/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..c6cdb4fce10 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: "RecruitIntel"; font-size: 32px; } } diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..661e768fb3e 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -18,4 +18,9 @@ Person *--> Name Person *--> Phone Person *--> Email Person *--> Address +Person *--> JobPosition +Person *--> Notes +Person *--> Team +Person *--> StartTime +Person *--> Duration @enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..0051ab0c290 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -18,7 +18,12 @@ Class Address Class Email Class Name Class Phone +Class JobPosition +Class Notes +Class Team Class Tag +Class StartTime +Class Duration Class I #FFFFFF } @@ -41,7 +46,12 @@ Person *--> Name Person *--> Phone Person *--> Email Person *--> Address +Person *--> JobPosition +Person *--> Notes Person *--> "*" Tag +Person *--> StartTime +Person *--> Duration +Person *--> Team Person -[hidden]up--> I UniquePersonList -[hidden]right-> I @@ -49,6 +59,7 @@ UniquePersonList -[hidden]right-> I Name -[hidden]right-> Phone Phone -[hidden]right-> Address Address -[hidden]right-> Email +Email -[hidden]right-> Notes ModelManager --> "~* filtered" Person @enduml diff --git a/docs/diagrams/UndoSequenceDiagram-Logic.puml b/docs/diagrams/UndoSequenceDiagram-Logic.puml index e57368c5159..f80195c80fd 100644 --- a/docs/diagrams/UndoSequenceDiagram-Logic.puml +++ b/docs/diagrams/UndoSequenceDiagram-Logic.puml @@ -6,6 +6,7 @@ box Logic LOGIC_COLOR_T1 participant ":LogicManager" as LogicManager LOGIC_COLOR participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR participant "u:UndoCommand" as UndoCommand LOGIC_COLOR +participant "o:DeleteCommand" as OtherCommand LOGIC_COLOR end box box Model MODEL_COLOR_T1 @@ -30,7 +31,10 @@ deactivate AddressBookParser LogicManager -> UndoCommand : execute() activate UndoCommand -UndoCommand -> Model : undoAddressBook() +UndoCommand -> OtherCommand : undo() +activate OtherCommand + +OtherCommand -> Model : undoAddressBook() activate Model Model --> UndoCommand diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..5a3f67fbfd0 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/ClassifyScreenshot.png b/docs/images/ClassifyScreenshot.png new file mode 100644 index 00000000000..69c3a5327a9 Binary files /dev/null and b/docs/images/ClassifyScreenshot.png differ diff --git a/docs/images/FindScreenshot.png b/docs/images/FindScreenshot.png new file mode 100644 index 00000000000..97db3bb6127 Binary files /dev/null and b/docs/images/FindScreenshot.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..22da8d7f8a1 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..99f88d326be 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/bryantjandra.png b/docs/images/bryantjandra.png new file mode 100644 index 00000000000..2532534c08e Binary files /dev/null and b/docs/images/bryantjandra.png differ diff --git a/docs/images/classifyTagAndRenameResult.png b/docs/images/classifyTagAndRenameResult.png new file mode 100644 index 00000000000..9bf64dc9a4d Binary files /dev/null and b/docs/images/classifyTagAndRenameResult.png differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png index 235da1c273e..caa51854f9d 100644 Binary files a/docs/images/findAlexDavidResult.png and b/docs/images/findAlexDavidResult.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..2643014d1d1 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/hzk-lab.png b/docs/images/hzk-lab.png new file mode 100644 index 00000000000..e3d7a0909e2 Binary files /dev/null and b/docs/images/hzk-lab.png differ diff --git a/docs/images/lsmnbmnc.png b/docs/images/lsmnbmnc.png new file mode 100644 index 00000000000..cddda1bb681 Binary files /dev/null and b/docs/images/lsmnbmnc.png differ diff --git a/docs/images/prog-neuro-com.png b/docs/images/prog-neuro-com.png new file mode 100755 index 00000000000..b625572f694 Binary files /dev/null and b/docs/images/prog-neuro-com.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..a68c4ada1ad 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,21 @@ --- layout: page -title: AddressBook Level-3 +title: RecruitIntel --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![CI Status](https://github.com/AY2425S2-CS2103T-F14-3/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2425S2-CS2103T-F14-3/tp/actions) +[![codecov](https://codecov.io/gh/AY2425S2-CS2103T-F14-3/tp/branch/master/graph/badge.svg)](https://codecov.io/gh/AY2425S2-CS2103T-F14-3/tp) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +RecruitIntel is a **desktop application designed specifically for Apple's HR recruiters** to efficiently manage candidate information. While offering an easy-to-use visual interface, it features powerful text commands that help process candidate information considerably faster than traditional mouse-based applications. -* 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 in using RecruitIntel, 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. **Acknowledgements** +* This project is based on the AddressBook-Level3 project created by +the [SE-EDU initiative](https://se-education.org). * Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) diff --git a/docs/team/bryantjandra.md b/docs/team/bryantjandra.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/team/gonghaozhen.md b/docs/team/gonghaozhen.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md index 773a07794e2..edfc0f55858 100644 --- a/docs/team/johndoe.md +++ b/docs/team/johndoe.md @@ -5,7 +5,7 @@ title: John Doe's Project Portfolio Page ### Project: AddressBook Level 3 -AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. +AddressBook - Level 3 is a desktop RecruitIntel application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. Given below are my contributions to the project. diff --git a/docs/team/prog-neuro-com.md b/docs/team/prog-neuro-com.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 678ddc8c218..f623c392f7b 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -48,7 +48,7 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing RecruitIntel ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -68,9 +68,9 @@ public void init() throws Exception { } /** - * Returns a {@code ModelManager} with the data from {@code storage}'s address book and {@code userPrefs}.
- * The data from the sample address book will be used instead if {@code storage}'s address book is not found, - * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. + * Returns a {@code ModelManager} with the data from {@code storage}'s RecruitIntel and {@code userPrefs}.
+ * The data from the sample RecruitIntel will be used instead if {@code storage}'s RecruitIntel is not found, + * or an empty RecruitIntel will be used instead if errors occur when reading {@code storage}'s RecruitIntel. */ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { logger.info("Using data file : " + storage.getAddressBookFilePath()); @@ -81,12 +81,12 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { addressBookOptional = storage.readAddressBook(); if (!addressBookOptional.isPresent()) { logger.info("Creating a new data file " + storage.getAddressBookFilePath() - + " populated with a sample AddressBook."); + + " populated with a sample RecruitIntel."); } initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); } catch (DataLoadingException e) { logger.warning("Data file at " + storage.getAddressBookFilePath() + " could not be loaded." - + " Will be starting with an empty AddressBook."); + + " Will be starting with an empty RecruitIntel."); initialData = new AddressBook(); } @@ -176,7 +176,7 @@ public void start(Stage primaryStage) { @Override public void stop() { - logger.info("============================ [ Stopping AddressBook ] ============================="); + logger.info("============================ [ Stopping RecruitIntel ] ============================="); try { storage.saveUserPrefs(model.getUserPrefs()); } catch (IOException e) { diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/address/commons/core/LogsCenter.java index 8cf8e15a0f0..429c00d30bc 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/address/commons/core/LogsCenter.java @@ -20,7 +20,7 @@ public class LogsCenter { private static final int MAX_FILE_COUNT = 5; private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB - private static final String LOG_FILE = "addressbook.log"; + private static final String LOG_FILE = "recruitintel.log"; private static final Logger logger; // logger for this class private static Logger baseLogger; // to be used as the parent of all other loggers created by this class. private static Level currentLogLevel = Level.INFO; diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/seedu/address/commons/util/FileUtil.java index b1e2767cdd9..023e42c6f07 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/seedu/address/commons/util/FileUtil.java @@ -18,7 +18,7 @@ public static boolean isFileExists(Path file) { } /** - * Returns true if {@code path} can be converted into a {@code Path} via {@link Paths#get(String)}, + * Returns true if {@code path} can be converted into a {@code Path} via {@code Paths.get()}, * otherwise returns false. * @param path A string representing the file path. Cannot be null. */ diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..06e44f2dcd2 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -30,11 +30,11 @@ public interface Logic { */ ReadOnlyAddressBook getAddressBook(); - /** Returns an unmodifiable view of the filtered list of persons */ + /** Returns an unmodifiable view of the filtered list of candidates */ ObservableList getFilteredPersonList(); /** - * Returns the user prefs' address book file path. + * Returns the user prefs' RecruitIntel file path. */ Path getAddressBookFilePath(); diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..e43da9615e7 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -14,10 +14,22 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = + "The candidate index provided is out of bounds. Please provide a valid index."; + public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d candidates listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = - "Multiple values specified for the following single-valued field(s): "; + "Multiple values specified for the following single-valued field(s): "; + public static final String MESSAGE_NOTES_CHARACTER_LIMIT_EXCEEDED = + "Notes cannot exceed 450 characters. Current length: %1$d characters."; + public static final String MESSAGE_NO_COMMAND_TO_UNDO = "There is no command to undo."; + + // Constants for person formatting + private static final String FORMAT_PHONE_PREFIX = "; Phone: "; + private static final String FORMAT_EMAIL_PREFIX = "; Email: "; + private static final String FORMAT_ADDRESS_PREFIX = "; Address: "; + private static final String FORMAT_JOB_POSITION_PREFIX = "; Job Position: "; + private static final String FORMAT_TEAM_PREFIX = "; Team: "; + private static final String FORMAT_TAGS_PREFIX = "; Tags: "; /** * Returns an error message indicating the duplicate prefixes. @@ -32,20 +44,23 @@ public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePref } /** - * Formats the {@code person} for display to the user. + * Formats the {@code candidate} for display to the user. */ public static String format(Person person) { final StringBuilder builder = new StringBuilder(); builder.append(person.getName()) - .append("; Phone: ") + .append(FORMAT_PHONE_PREFIX) .append(person.getPhone()) - .append("; Email: ") + .append(FORMAT_EMAIL_PREFIX) .append(person.getEmail()) - .append("; Address: ") + .append(FORMAT_ADDRESS_PREFIX) .append(person.getAddress()) - .append("; Tags: "); + .append(FORMAT_JOB_POSITION_PREFIX) + .append(person.getJobPosition()) + .append(FORMAT_TEAM_PREFIX) + .append(person.getTeam()) + .append(FORMAT_TAGS_PREFIX); 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..fb71e20a367 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -3,9 +3,11 @@ 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_JOB_POSITION; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TEAM; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; @@ -14,29 +16,35 @@ import seedu.address.model.person.Person; /** - * Adds a person to the address book. + * Adds a Candidate to RecruitIntel. */ public class AddCommand extends Command { public static final String COMMAND_WORD = "add"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a candidate to RecruitIntel. " + "Parameters: " + PREFIX_NAME + "NAME " + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_JOB_POSITION + "JOB POSITION " + + PREFIX_TEAM + "TEAM " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "98765432 " + PREFIX_EMAIL + "johnd@example.com " + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_JOB_POSITION + "Software Engineer " + + PREFIX_TEAM + "iOS Development " + + PREFIX_TAG + "experienced " + + PREFIX_TAG + "python"; - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + public static final String MESSAGE_SUCCESS = "New candidate added: %1$s"; + public static final String MESSAGE_DUPLICATE_PERSON = "Cannot add duplicate candidate: " + + "Email address '%1$s' is already in use.\n" + + "Each candidate must have a unique email address."; private final Person toAdd; @@ -53,13 +61,15 @@ public CommandResult execute(Model model) throws CommandException { requireNonNull(model); if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + throw new CommandException(String.format(MESSAGE_DUPLICATE_PERSON, toAdd.getEmail().toString())); } model.addPerson(toAdd); + model.commit(); return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/logic/commands/ClassifyCommand.java b/src/main/java/seedu/address/logic/commands/ClassifyCommand.java new file mode 100644 index 00000000000..7fe5b94b6d9 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ClassifyCommand.java @@ -0,0 +1,76 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Classifies and lists all candidates in RecruitIntel based on specified criteria (tags, team, or job position). + * Multiple criteria can be specified (using AND logic). + * Keyword matching is case-insensitive. + */ +public class ClassifyCommand extends Command { + + public static final String COMMAND_WORD = "classify"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all candidates matching ALL specified criteria " + + "and displays them as a list with index numbers.\n" + + "Parameters: [t/TAG]... [tm/TEAM] [j/JOB_POSITION]\n" + + "Note: Multiple tags (t/) are allowed and will be combined with AND logic.\n" + + " Only one team (tm/) and one job position (j/) are allowed.\n" + + "Examples:\n" + + " " + COMMAND_WORD + " t/python\n" + + " " + COMMAND_WORD + " t/python t/experienced\n" + + " " + COMMAND_WORD + " tm/Engineering\n" + + " " + COMMAND_WORD + " j/Software Engineer\n" + + " " + COMMAND_WORD + " t/mobile t/experienced j/Frontend Developer tm/TikTok Live"; + + public static final String MESSAGE_MULTIPLE_JOB_POSITIONS = "Only one job position (j/) is allowed."; + public static final String MESSAGE_MULTIPLE_TEAMS = "Only one team (tm/) is allowed."; + + private final List> predicates; + + public ClassifyCommand(List> predicates) { + this.predicates = predicates; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + + // Combine all predicates with AND logic + Predicate combinedPredicate = person -> + predicates.stream().allMatch(pred -> pred.test(person)); + + model.updateFilteredPersonList(combinedPredicate); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ClassifyCommand)) { + return false; + } + + ClassifyCommand otherClassifyCommand = (ClassifyCommand) other; + return predicates.equals(otherClassifyCommand.predicates); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicates", predicates) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..b6f9be7a6e1 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -6,18 +6,20 @@ import seedu.address.model.Model; /** - * Clears the address book. + * Clears the RecruitIntel. */ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - + public static final String MESSAGE_SUCCESS = "RecruitIntel has been cleared!"; @Override public CommandResult execute(Model model) { requireNonNull(model); model.setAddressBook(new AddressBook()); + model.commit(); + 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..b8ccdf7a369 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -12,28 +12,39 @@ import seedu.address.model.person.Person; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes a candidate identified using it's displayed index from the RecruitIntel. */ public class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" + + ": Deletes the candidates identified by the index number used in the displayed candidates list.\n" + "Parameters: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Candidate: %1$s"; private final Index targetIndex; + private Person deletedPerson = null; + public DeleteCommand(Index targetIndex) { this.targetIndex = targetIndex; } + /** + * Creates a DeleteCommand with a person. + */ + public DeleteCommand(Person person) { + this.targetIndex = Index.fromZeroBased(0); + this.deletedPerson = person; + } + @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); if (targetIndex.getZeroBased() >= lastShownList.size()) { @@ -42,6 +53,10 @@ public CommandResult execute(Model model) throws CommandException { Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); model.deletePerson(personToDelete); + deletedPerson = personToDelete; + + model.commit(); + 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..4e7f5483b16 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -3,9 +3,11 @@ 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_JOB_POSITION; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TEAM; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import java.util.Collections; @@ -22,42 +24,52 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.person.Address; +import seedu.address.model.person.Duration; import seedu.address.model.person.Email; +import seedu.address.model.person.JobPosition; import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.StartTime; +import seedu.address.model.person.Team; import seedu.address.model.tag.Tag; /** - * Edits the details of an existing person in the address book. + * Edits the details of an existing candidate in the RecruitIntel. */ public class EditCommand extends Command { public static final String COMMAND_WORD = "edit"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the candidate identified " + + "by the index number used in the displayed candidate list. " + "Existing values will be overwritten by the input values.\n" + "Parameters: INDEX (must be a positive integer) " + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_JOB_POSITION + "JOB POSITION] " + + "[" + PREFIX_TEAM + "TEAM] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " + PREFIX_EMAIL + "johndoe@example.com"; - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; + public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Candidate: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_DUPLICATE_PERSON = "This candidate already exists in RecruitIntel."; private final Index index; private final EditPersonDescriptor editPersonDescriptor; + private Person personToEdit; + private Person editedPerson; + /** - * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with + * @param index of the candidate in the filtered candidate list to edit + * @param editPersonDescriptor details to edit the candidate with */ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { requireNonNull(index); @@ -73,11 +85,12 @@ public CommandResult execute(Model model) throws CommandException { List lastShownList = model.getFilteredPersonList(); if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + throw new CommandException( + Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + personToEdit = lastShownList.get(index.getZeroBased()); + editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { throw new CommandException(MESSAGE_DUPLICATE_PERSON); @@ -85,6 +98,9 @@ public CommandResult execute(Model model) throws CommandException { model.setPerson(personToEdit, editedPerson); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + model.commit(); + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); } @@ -99,9 +115,15 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + JobPosition updatedJobPosition = editPersonDescriptor.getJobPosition().orElse(personToEdit.getJobPosition()); + Team updatedTeam = editPersonDescriptor.getTeam().orElse(personToEdit.getTeam()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + Notes notes = personToEdit.getNotes(); + StartTime startTime = personToEdit.getStartTime(); + Duration duration = personToEdit.getDuration(); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedJobPosition, + updatedTeam, updatedTags, notes, startTime, duration); } @Override @@ -129,15 +151,20 @@ public String toString() { } /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. + * Stores the details to edit the candidate with. Each non-empty field value will replace the + * corresponding field value of the candidate. */ public static class EditPersonDescriptor { private Name name; private Phone phone; private Email email; private Address address; + private JobPosition jobPosition; + private Team team; private Set tags; + private Notes notes; + private StartTime startTime; + private Duration duration; public EditPersonDescriptor() {} @@ -150,14 +177,30 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setPhone(toCopy.phone); setEmail(toCopy.email); setAddress(toCopy.address); + setJobPosition(toCopy.jobPosition); + setTeam(toCopy.team); setTags(toCopy.tags); + setNotes(toCopy.notes); + setStartTime(toCopy.startTime); + setDuration(toCopy.duration); + } + + private void setNotes(Notes notes) { + this.notes = notes; + } + + private void setStartTime(StartTime startTime) { + this.startTime = startTime; + } + private void setDuration(Duration duration) { + this.duration = duration; } /** * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, address, jobPosition, team, tags); } public void setName(Name name) { @@ -192,6 +235,22 @@ public Optional
getAddress() { return Optional.ofNullable(address); } + public void setJobPosition(JobPosition jobPosition) { + this.jobPosition = jobPosition; + } + + public Optional getJobPosition() { + return Optional.ofNullable(jobPosition); + } + + public void setTeam(Team team) { + this.team = team; + } + + public Optional getTeam() { + return Optional.ofNullable(team); + } + /** * Sets {@code tags} to this object's {@code tags}. * A defensive copy of {@code tags} is used internally. @@ -225,6 +284,8 @@ public boolean equals(Object other) { && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) && Objects.equals(address, otherEditPersonDescriptor.address) + && Objects.equals(jobPosition, otherEditPersonDescriptor.jobPosition) + && Objects.equals(team, otherEditPersonDescriptor.team) && Objects.equals(tags, otherEditPersonDescriptor.tags); } @@ -235,7 +296,12 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("jobPosition", jobPosition) + .add("team", team) .add("tags", tags) + .add("notes", notes) + .add("interview time", startTime) + .add("duration", duration) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..2c5d264166f 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -9,7 +9,7 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting RecruitIntel as requested ..."; @Override public CommandResult execute(Model model) { diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..6e079e6aa37 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -8,14 +8,14 @@ import seedu.address.model.person.NameContainsKeywordsPredicate; /** - * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Finds and lists all candidates in RecruitIntel whose name contains any of the argument keywords. * Keyword matching is case insensitive. */ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all candidates whose names contain any of " + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + "Example: " + COMMAND_WORD + " alice bob charlie"; diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..22163a7a028 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -18,4 +18,5 @@ public class HelpCommand extends Command { public CommandResult execute(Model model) { return new CommandResult(SHOWING_HELP_MESSAGE, true, false); } + } diff --git a/src/main/java/seedu/address/logic/commands/InterviewCommand.java b/src/main/java/seedu/address/logic/commands/InterviewCommand.java new file mode 100644 index 00000000000..798a1633097 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/InterviewCommand.java @@ -0,0 +1,105 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Duration; +import seedu.address.model.person.Person; +import seedu.address.model.person.StartTime; + +/** + * Sets the interview time (start time + duration) for a candidate in RecruitIntel. + */ +public class InterviewCommand extends Command { + + public static final String COMMAND_WORD = "interview"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sets interview time for the candidate identified " + + "by the index number used in the displayed candidate list.\n" + + "Parameters: INDEX (must be a positive integer) START_TIME (yyyy-MM-dd HH:mm) DURATION (multiple of 5)\n" + + "Example: " + COMMAND_WORD + " 1 2025-04-01 14:00 30"; + + public static final String MESSAGE_SET_INTERVIEW_SUCCESS = "Interview set for candidate #%1$d: " + + "Start = %2$s, Duration = %3$d minutes"; + + private final Index targetIndex; + private StartTime startTime; + private Duration duration; + + private Person updatedPerson; + private Person personToUpdate; + + + /** + * @param targetIndex index of candidate to set interview for + * @param startTime start time of the interview + * @param duration duration of the interview + */ + public InterviewCommand(Index targetIndex, StartTime startTime, Duration duration) { + requireNonNull(targetIndex); + requireNonNull(startTime); + requireNonNull(duration); + + this.targetIndex = targetIndex; + this.startTime = startTime; + this.duration = duration; + } + + @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); + } + + personToUpdate = lastShownList.get(targetIndex.getZeroBased()); + + // Construct updated person + updatedPerson = new Person( + personToUpdate.getName(), + personToUpdate.getPhone(), + personToUpdate.getEmail(), + personToUpdate.getAddress(), + personToUpdate.getJobPosition(), + personToUpdate.getTeam(), + personToUpdate.getTags(), + personToUpdate.getNotes(), + startTime, + duration + ); + + model.setPerson(personToUpdate, updatedPerson); + + model.commit(); + + return new CommandResult(String.format(MESSAGE_SET_INTERVIEW_SUCCESS, + targetIndex.getOneBased(), startTime.value, duration.getDurationInMinutes())); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof InterviewCommand + && targetIndex.equals(((InterviewCommand) other).targetIndex) + && startTime.equals(((InterviewCommand) other).startTime) + && duration.equals(((InterviewCommand) other).duration)); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .add("startTime", startTime) + .add("duration", duration) + .toString(); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..2b7911f794d 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -6,13 +6,13 @@ import seedu.address.model.Model; /** - * Lists all persons in the address book to the user. + * Lists all candidates in the RecruitIntel to the HR. */ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; - public static final String MESSAGE_SUCCESS = "Listed all persons"; + public static final String MESSAGE_SUCCESS = "Listed all candidates"; @Override @@ -21,4 +21,5 @@ public CommandResult execute(Model model) { model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); return new CommandResult(MESSAGE_SUCCESS); } + } diff --git a/src/main/java/seedu/address/logic/commands/NotesCommand.java b/src/main/java/seedu/address/logic/commands/NotesCommand.java new file mode 100644 index 00000000000..5f4af169205 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/NotesCommand.java @@ -0,0 +1,109 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Notes; +import seedu.address.model.person.Person; + +/** + * Adds interviewer notes to an existing candidate in RecruitIntel. + */ +public class NotesCommand extends Command { + + public static final String COMMAND_WORD = "note"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds interviewer notes to the candidate identified " + + "by the index number used in the displayed candidate list.\n" + + "Parameters: INDEX (must be a positive integer) NOTE_TEXT (maximum 450 characters)\n" + + "Example: " + COMMAND_WORD + " 1 Strong backend experience, but lacks iOS exposure."; + + public static final String MESSAGE_ADD_NOTES_SUCCESS = "Note added to candidate #%1$d: \"%2$s\""; + + private final Index targetIndex; + private Notes notes; + private Person targetPerson; + private Notes lastNotes; + + /** + * @param targetIndex of the candidate in the filtered candidate list to add notes to + * @param notes the notes to add to the candidate + */ + public NotesCommand(Index targetIndex, Notes notes) { + requireNonNull(targetIndex); + requireNonNull(notes); + + this.targetIndex = targetIndex; + this.notes = notes; + } + + /** + * Creates a new person with updated notes while preserving all other fields. + */ + private Person createPersonWithNotes(Person basePerson, Notes newNotes) { + return new Person( + basePerson.getName(), + basePerson.getPhone(), + basePerson.getEmail(), + basePerson.getAddress(), + basePerson.getJobPosition(), + basePerson.getTeam(), + basePerson.getTags(), + newNotes, + basePerson.getStartTime(), + basePerson.getDuration() + ); + } + + @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 personToAddNotesTo = lastShownList.get(targetIndex.getZeroBased()); + targetPerson = personToAddNotesTo; + lastNotes = personToAddNotesTo.getNotes(); + + Person updatedPerson = createPersonWithNotes(personToAddNotesTo, notes); + model.setPerson(personToAddNotesTo, updatedPerson); + targetPerson = updatedPerson; + + model.commit(); + + return new CommandResult(String.format(MESSAGE_ADD_NOTES_SUCCESS, targetIndex.getOneBased(), notes.value)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof NotesCommand)) { + return false; + } + + NotesCommand otherNotesCommand = (NotesCommand) other; + return targetIndex.equals(otherNotesCommand.targetIndex) + && notes.equals(otherNotesCommand.notes); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .add("notes", notes) + .toString(); + } +} 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..3f2a1c39c7d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -0,0 +1,46 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; + +/** + * Redoes the most recent command that was undone. + */ +public class RedoCommand extends Command { + + public static final String COMMAND_WORD = "redo"; + public static final String MESSAGE_SUCCESS = "Redo successful!"; + public static final String MESSAGE_REDO_NOT_AVAILABLE = "No command to redo!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Redoes the last command that was undone.\n" + + "Example: " + COMMAND_WORD; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + try { + model.redo(); + } catch (ModelManager.NoRedoableStateException e) { + throw new CommandException(MESSAGE_REDO_NOT_AVAILABLE); + } + + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || other instanceof RedoCommand; // instanceof handles nulls + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SortCommand.java b/src/main/java/seedu/address/logic/commands/SortCommand.java new file mode 100644 index 00000000000..f53ed9fdc67 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SortCommand.java @@ -0,0 +1,47 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Sorts all candidates in RecruitIntel by their start time in ascending order. + */ +public class SortCommand extends Command { + + public static final String COMMAND_WORD = "sort"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sorts all candidates by their start time " + + "in ascending order and displays them as a list.\n" + + "Example: " + COMMAND_WORD; + + public static final String MESSAGE_SORT_SUCCESS = "Sorted all candidates by their " + + "Interviews' Start Time."; + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.sortFilteredPersonList(this::compareByStartTime); + return new CommandResult(MESSAGE_SORT_SUCCESS); + } + + /** + * Compares two persons by their interview start time. + */ + private int compareByStartTime(Person p1, Person p2) { + return p1.getStartTime().compareTo(p2.getStartTime()); + } + + @Override + public boolean equals(Object other) { + return other instanceof SortCommand; // no internal state, all instances are equal + } + + @Override + public String toString() { + return new ToStringBuilder(this).toString(); + } + +} 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..7f99d89c381 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -0,0 +1,49 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; + +/** + * Undo the most recent command that modifies the data. + */ +public class UndoCommand extends Command { + + public static final String COMMAND_WORD = "undo"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Undo the most recent command that modifies the data.\n" + + "Example: " + COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Undo successful!"; + + public static final String MESSAGE_UNDO_NOT_AVAILABLE = "No command to undo!"; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + try { + model.undo(); + } catch (ModelManager.NoUndoableStateException e) { + throw new CommandException(MESSAGE_UNDO_NOT_AVAILABLE); + } + + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || other instanceof UndoCommand; // instanceof handles nulls + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .toString(); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java index a16bd14f2cd..f44c0c19186 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java @@ -1,5 +1,7 @@ package seedu.address.logic.commands.exceptions; +import seedu.address.logic.commands.Command; + /** * Represents an error which occurs during execution of a {@link Command}. */ diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..4bc86fdbcd7 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -3,9 +3,11 @@ 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_JOB_POSITION; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TEAM; import java.util.Set; import java.util.stream.Stream; @@ -14,9 +16,11 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.JobPosition; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Team; import seedu.address.model.tag.Tag; /** @@ -30,24 +34,49 @@ public class AddCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public AddCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentMultimap argMultimap = tokenizeArguments(args); + validatePrefixes(argMultimap); + Person person = createPerson(argMultimap); + return new AddCommand(person); + } - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + /** + * Tokenizes the command arguments with the required prefixes. + */ + private ArgumentMultimap tokenizeArguments(String args) { + return ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_JOB_POSITION, PREFIX_TEAM, PREFIX_TAG); + } + + /** + * Validates that all required prefixes are present and no duplicates exist. + * @throws ParseException if the user input does not conform to the expected format + */ + private void validatePrefixes(ArgumentMultimap argMultimap) throws ParseException { + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_JOB_POSITION, PREFIX_TEAM) || !argMultimap.getPreamble().isEmpty()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_JOB_POSITION, PREFIX_TEAM); + } + + /** + * Creates a Person object from the given argument multimap. + * @throws ParseException if there are problems parsing the field values + */ + private Person createPerson(ArgumentMultimap argMultimap) throws ParseException { 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()); + JobPosition jobPosition = ParserUtil.parseJobPosition(argMultimap.getValue(PREFIX_JOB_POSITION).get()); + Team team = ParserUtil.parseTeam(argMultimap.getValue(PREFIX_TEAM).get()); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); + return new Person(name, phone, email, address, jobPosition, team, tagList); } /** diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..bd152c3786c 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -9,6 +9,7 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.ClassifyCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.DeleteCommand; @@ -16,7 +17,12 @@ import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.InterviewCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.NotesCommand; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.commands.UndoCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -77,6 +83,24 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case InterviewCommand.COMMAND_WORD: + return new InterviewCommandParser().parse(arguments); + + case NotesCommand.COMMAND_WORD: + return new NotesCommandParser().parse(arguments); + + case UndoCommand.COMMAND_WORD: + return new UndoCommandParser().parse(arguments); + + case RedoCommand.COMMAND_WORD: + return new RedoCommandParser().parse(arguments); + + case ClassifyCommand.COMMAND_WORD: + return new ClassifyCommandParser().parse(arguments); + + case SortCommand.COMMAND_WORD: + return new SortCommandParser().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/ClassifyCommandParser.java b/src/main/java/seedu/address/logic/parser/ClassifyCommandParser.java new file mode 100644 index 00000000000..4c50d828f5c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ClassifyCommandParser.java @@ -0,0 +1,131 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_JOB_POSITION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TEAM; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import seedu.address.logic.commands.ClassifyCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.JobPositionContainsKeywordsPredicate; +import seedu.address.model.person.Person; +import seedu.address.model.person.TagsContainsKeywordsPredicate; +import seedu.address.model.person.TeamContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new ClassifyCommand object + */ +public class ClassifyCommandParser implements Parser { + + private static final String MESSAGE_EMPTY_TAG = "Tag value cannot be empty"; + private static final String MESSAGE_EMPTY_TEAM = "Team value cannot be empty"; + private static final String MESSAGE_EMPTY_JOB = "Job position value cannot be empty"; + + /** + * Parses the given {@code String} of arguments in the context of the ClassifyCommand + * and returns a ClassifyCommand object for execution. + * @throws ParseException if the user input does not conform to the expected format + */ + public ClassifyCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = tokenizeArguments(args); + validateAtLeastOnePrefix(argMultimap); + validateSingleJobAndTeam(argMultimap); + List> predicates = createPredicates(argMultimap); + return new ClassifyCommand(predicates); + } + + /** + * Validates that there is at most one job position and one team. + * @throws ParseException if multiple job positions or teams are found + */ + private void validateSingleJobAndTeam(ArgumentMultimap argMultimap) throws ParseException { + if (argMultimap.getAllValues(PREFIX_JOB_POSITION).size() > 1) { + throw new ParseException(ClassifyCommand.MESSAGE_MULTIPLE_JOB_POSITIONS); + } + if (argMultimap.getAllValues(PREFIX_TEAM).size() > 1) { + throw new ParseException(ClassifyCommand.MESSAGE_MULTIPLE_TEAMS); + } + } + + /** + * Tokenizes the command arguments with the required prefixes. + */ + private ArgumentMultimap tokenizeArguments(String args) { + return ArgumentTokenizer.tokenize(args, PREFIX_TAG, PREFIX_TEAM, PREFIX_JOB_POSITION); + } + + /** + * Validates that at least one prefix is present in the ArgumentMultimap. + * @throws ParseException if no prefixes are present + */ + private void validateAtLeastOnePrefix(ArgumentMultimap argMultimap) throws ParseException { + if (!argMultimap.getValue(PREFIX_TAG).isPresent() + && !argMultimap.getValue(PREFIX_TEAM).isPresent() + && !argMultimap.getValue(PREFIX_JOB_POSITION).isPresent()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ClassifyCommand.MESSAGE_USAGE)); + } + } + + /** + * Creates a list of predicates based on the provided arguments. + * @throws ParseException if any of the provided values are empty + */ + private List> createPredicates(ArgumentMultimap argMultimap) throws ParseException { + List> predicates = new ArrayList<>(); + addTagPredicateIfPresent(argMultimap, predicates); + addTeamPredicateIfPresent(argMultimap, predicates); + addJobPredicateIfPresent(argMultimap, predicates); + return predicates; + } + + /** + * Adds tag predicates to the list if tag prefix is present. + * Each tag gets its own predicate to ensure AND logic between tags. + */ + private void addTagPredicateIfPresent(ArgumentMultimap argMultimap, List> predicates) + throws ParseException { + List tagValues = argMultimap.getAllValues(PREFIX_TAG); + for (String tagValue : tagValues) { + String trimmedTag = tagValue.trim(); + if (trimmedTag.isEmpty()) { + throw new ParseException(MESSAGE_EMPTY_TAG); + } + predicates.add(new TagsContainsKeywordsPredicate(List.of(trimmedTag))); + } + } + + /** + * Adds a team predicate to the list if team prefix is present. + */ + private void addTeamPredicateIfPresent(ArgumentMultimap argMultimap, List> predicates) + throws ParseException { + Optional teamValue = argMultimap.getValue(PREFIX_TEAM); + if (teamValue.isPresent()) { + String trimmedTeam = teamValue.get().trim(); + if (trimmedTeam.isEmpty()) { + throw new ParseException(MESSAGE_EMPTY_TEAM); + } + predicates.add(new TeamContainsKeywordsPredicate(List.of(trimmedTeam))); + } + } + + /** + * Adds a job position predicate to the list if job position prefix is present. + */ + private void addJobPredicateIfPresent(ArgumentMultimap argMultimap, List> predicates) + throws ParseException { + Optional jobValue = argMultimap.getValue(PREFIX_JOB_POSITION); + if (jobValue.isPresent()) { + String trimmedJob = jobValue.get().trim(); + if (trimmedJob.isEmpty()) { + throw new ParseException(MESSAGE_EMPTY_JOB); + } + predicates.add(new JobPositionContainsKeywordsPredicate(List.of(trimmedJob))); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..dbe8201e277 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -11,5 +11,7 @@ public class CliSyntax { public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_JOB_POSITION = new Prefix("j/"); + public static final Prefix PREFIX_TEAM = new Prefix("tm/"); } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..1fdbd6a4127 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -4,9 +4,11 @@ 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_JOB_POSITION; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TEAM; import java.util.Collection; import java.util.Collections; @@ -31,19 +33,48 @@ 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); + ArgumentMultimap argMultimap = tokenizeArguments(args); + Index index = parseIndex(argMultimap); + validateNoDuplicatePrefixes(argMultimap); - Index index; + EditPersonDescriptor editPersonDescriptor = createEditPersonDescriptor(argMultimap); + validateAtLeastOneField(editPersonDescriptor); + return new EditCommand(index, editPersonDescriptor); + } + + /** + * Tokenizes the arguments with all available prefixes. + */ + private ArgumentMultimap tokenizeArguments(String args) { + return ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_JOB_POSITION, PREFIX_TEAM, PREFIX_TAG); + } + + /** + * Parses the index from the argument multimap. + * @throws ParseException if the index is invalid + */ + private Index parseIndex(ArgumentMultimap argMultimap) throws ParseException { try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); + return ParserUtil.parseIndex(argMultimap.getPreamble()); } catch (ParseException pe) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } + } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + /** + * Validates that there are no duplicate prefixes. + */ + private void validateNoDuplicatePrefixes(ArgumentMultimap argMultimap) throws ParseException { + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_JOB_POSITION, PREFIX_TEAM); + } + /** + * Creates an EditPersonDescriptor from the provided arguments. + */ + private EditPersonDescriptor createEditPersonDescriptor(ArgumentMultimap argMultimap) throws ParseException { EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); if (argMultimap.getValue(PREFIX_NAME).isPresent()) { @@ -58,13 +89,25 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } + if (argMultimap.getValue(PREFIX_JOB_POSITION).isPresent()) { + String jobPosition = argMultimap.getValue(PREFIX_JOB_POSITION).get(); + editPersonDescriptor.setJobPosition(ParserUtil.parseJobPosition(jobPosition)); + } + if (argMultimap.getValue(PREFIX_TEAM).isPresent()) { + editPersonDescriptor.setTeam(ParserUtil.parseTeam(argMultimap.getValue(PREFIX_TEAM).get())); + } parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + return editPersonDescriptor; + } + + /** + * Validates that at least one field is edited. + */ + private void validateAtLeastOneField(EditPersonDescriptor editPersonDescriptor) throws ParseException { if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); } - - return new EditCommand(index, editPersonDescriptor); } /** @@ -81,5 +124,4 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; return Optional.of(ParserUtil.parseTags(tagSet)); } - } diff --git a/src/main/java/seedu/address/logic/parser/InterviewCommandParser.java b/src/main/java/seedu/address/logic/parser/InterviewCommandParser.java new file mode 100644 index 00000000000..e06c663c036 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/InterviewCommandParser.java @@ -0,0 +1,74 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.InterviewCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Duration; +import seedu.address.model.person.StartTime; + +/** + * Parses input arguments and creates a new InterviewCommand object + */ +public class InterviewCommandParser implements Parser { + + private static final Logger logger = LogsCenter.getLogger(InterviewCommandParser.class); + + /** + * Parses the given {@code String} of arguments in the context of the InterviewCommand + * and returns an InterviewCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public InterviewCommand parse(String args) throws ParseException { + requireNonNull(args); + String trimmedArgs = args.trim(); + + String[] parts = trimmedArgs.split("\\s+"); + + if (parts.length < 4) { + logger.info("Insufficient arguments provided for interview command"); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE)); + } + + if (parts.length > 4) { + logger.info("Over arguments provided for interview command"); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE)); + } + + Index index; + try { + index = ParserUtil.parseIndex(parts[0]); + logger.fine("Parsed index: " + index.getOneBased()); + } catch (ParseException pe) { + logger.info("Invalid index input: " + parts[0]); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE), pe); + } + + // Reconstruct date and time string + String startTimeString = parts[1] + " " + parts[2]; + String durationString = parts[3]; + + if (!StartTime.isValidStartTime(startTimeString)) { + logger.info("Invalid start time format: " + startTimeString); + throw new ParseException(StartTime.MESSAGE_CONSTRAINTS); + } + + if (!Duration.isValidDuration(durationString)) { + logger.info("Invalid duration: " + durationString); + throw new ParseException(Duration.MESSAGE_CONSTRAINTS); + } + + StartTime startTime = new StartTime(startTimeString); + Duration duration = new Duration(durationString); + + logger.fine("Parsed start time: " + startTime.value); + logger.fine("Parsed duration: " + duration.getDurationInMinutes() + " minutes"); + + return new InterviewCommand(index, startTime, duration); + } +} diff --git a/src/main/java/seedu/address/logic/parser/NotesCommandParser.java b/src/main/java/seedu/address/logic/parser/NotesCommandParser.java new file mode 100644 index 00000000000..a7b25716315 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/NotesCommandParser.java @@ -0,0 +1,108 @@ +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.Messages.MESSAGE_NOTES_CHARACTER_LIMIT_EXCEEDED; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.NotesCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Notes; + +/** + * Parses input arguments and creates a new NotesCommand object + */ +public class NotesCommandParser implements Parser { + + private static final Logger logger = LogsCenter.getLogger(NotesCommandParser.class); + private static final int EXPECTED_PARTS = 2; + private static final int INDEX_PART = 0; + private static final int NOTES_PART = 1; + + /** + * Parses the given {@code String} of arguments in the context of the NotesCommand + * and returns a NotesCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public NotesCommand parse(String args) throws ParseException { + requireNonNull(args); + String trimmedArgs = args.trim(); + validateNonEmptyInput(trimmedArgs); + + String[] parts = splitInput(trimmedArgs); + Index index = parseIndex(parts[INDEX_PART]); + Notes notes = parseNotes(parts[NOTES_PART]); + + logger.fine("Created NotesCommand with index: " + index.getOneBased() + " and notes: " + notes.value); + return new NotesCommand(index, notes); + } + + /** + * Validates that the input is not empty. + * @throws ParseException if the input is empty + */ + private void validateNonEmptyInput(String trimmedArgs) throws ParseException { + if (trimmedArgs.isEmpty()) { + logger.info("Empty arguments provided for notes command"); + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, NotesCommand.MESSAGE_USAGE)); + } + } + + /** + * Splits the input into index and notes parts. + * @throws ParseException if the input cannot be split into exactly two parts + */ + private String[] splitInput(String trimmedArgs) throws ParseException { + String[] parts = trimmedArgs.split("\\s+", EXPECTED_PARTS); + if (parts.length < EXPECTED_PARTS) { + logger.info("Insufficient arguments provided for notes command"); + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, NotesCommand.MESSAGE_USAGE)); + } + return parts; + } + + /** + * Parses the index string into an Index object. + * @throws ParseException if the index string is invalid + */ + private Index parseIndex(String indexStr) throws ParseException { + try { + Index index = ParserUtil.parseIndex(indexStr); + logger.fine("Index parsed successfully: " + index.getOneBased()); + return index; + } catch (ParseException pe) { + logger.info("Failed to parse index: " + indexStr); + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, NotesCommand.MESSAGE_USAGE), pe); + } + } + + /** + * Parses the notes string into a Notes object. + * @throws ParseException if the notes exceed the character limit + */ + private Notes parseNotes(String notesText) throws ParseException { + String trimmedNotes = notesText.trim(); + validateNotesLength(trimmedNotes); + Notes notes = new Notes(trimmedNotes); + logger.fine("Notes parsed successfully: " + notes.value); + return notes; + } + + /** + * Validates that the notes do not exceed the maximum length. + * @throws ParseException if the notes exceed the character limit + */ + private void validateNotesLength(String notesText) throws ParseException { + if (notesText.length() > Notes.MAX_LENGTH) { + logger.info("Notes exceed character limit: " + notesText.length() + " characters"); + throw new ParseException( + String.format(MESSAGE_NOTES_CHARACTER_LIMIT_EXCEEDED, notesText.length())); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..adf5152dda4 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -11,8 +11,10 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.JobPosition; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.person.Team; import seedu.address.model.tag.Tag; /** @@ -28,6 +30,7 @@ public class ParserUtil { * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). */ public static Index parseIndex(String oneBasedIndex) throws ParseException { + requireNonNull(oneBasedIndex); String trimmedIndex = oneBasedIndex.trim(); if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { throw new ParseException(MESSAGE_INVALID_INDEX); @@ -95,6 +98,38 @@ public static Email parseEmail(String email) throws ParseException { return new Email(trimmedEmail); } + /** + * Parses a {@code String jobPosition} into a {@code JobPosition}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code jobPosition} is invalid. + */ + public static JobPosition parseJobPosition(String jobPosition) throws ParseException { + requireNonNull(jobPosition); + String trimmedJobPosition = jobPosition.trim(); + try { + return new JobPosition(trimmedJobPosition); + } catch (IllegalArgumentException e) { + throw new ParseException(e.getMessage()); + } + } + + /** + * Parses a {@code String team} into a {@code Team}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code team} is invalid. + */ + public static Team parseTeam(String team) throws ParseException { + requireNonNull(team); + String trimmedTeam = team.trim(); + try { + return new Team(trimmedTeam); + } catch (IllegalArgumentException e) { + throw new ParseException(e.getMessage()); + } + } + /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. diff --git a/src/main/java/seedu/address/logic/parser/RedoCommandParser.java b/src/main/java/seedu/address/logic/parser/RedoCommandParser.java new file mode 100644 index 00000000000..2cd51145480 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RedoCommandParser.java @@ -0,0 +1,34 @@ +package seedu.address.logic.parser; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new RedoCommand object. + */ +public class RedoCommandParser implements Parser { + private static final Logger logger = LogsCenter.getLogger(RedoCommandParser.class); + + /** + * Parses the given {@code String} of arguments in the context of the RedoCommand + * and returns a RedoCommand object for execution. + * + * @param args The arguments to parse. + * @return A RedoCommand object for execution. + * @throws ParseException If the user input does not conform the expected format. + */ + public RedoCommand parse(String args) throws ParseException { + logger.info("Parsing redo command"); + String trimmedArgs = args.trim(); + if (!trimmedArgs.isEmpty()) { + logger.info("Invalid arguments provided for redo command"); + throw new ParseException( + String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, RedoCommand.MESSAGE_USAGE)); + } + return new RedoCommand(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/SortCommandParser.java b/src/main/java/seedu/address/logic/parser/SortCommandParser.java new file mode 100644 index 00000000000..1cd2bfa3195 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SortCommandParser.java @@ -0,0 +1,20 @@ +package seedu.address.logic.parser; + +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new SortCommand object + */ +public class SortCommandParser implements Parser { + + @Override + public SortCommand parse(String args) throws ParseException { + // This command takes no arguments, so we can safely ignore args or throw if not empty + String trimmedArgs = args.trim(); + if (!trimmedArgs.isEmpty()) { + throw new ParseException("Sort command does not accept any arguments.\n" + SortCommand.MESSAGE_USAGE); + } + return new SortCommand(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/UndoCommandParser.java b/src/main/java/seedu/address/logic/parser/UndoCommandParser.java new file mode 100644 index 00000000000..0af9b6326e3 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/UndoCommandParser.java @@ -0,0 +1,31 @@ +package seedu.address.logic.parser; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.UndoCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new UndoCommand object + */ +public class UndoCommandParser implements Parser { + private static final Logger logger = LogsCenter.getLogger(UndoCommandParser.class); + + /** + * Parses the given {@code String} of arguments in the context of the UndoCommand + * and returns an UndoCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public UndoCommand parse(String args) throws ParseException { + logger.info("Parsing undo command"); + String trimmedArgs = args.trim(); + if (!trimmedArgs.isEmpty()) { + logger.info("Invalid arguments provided for undo command"); + throw new ParseException( + String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, UndoCommand.MESSAGE_USAGE)); + } + return new UndoCommand(); + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..2f613e49e88 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; +import java.util.Comparator; import java.util.List; import javafx.collections.ObservableList; @@ -10,7 +11,7 @@ import seedu.address.model.person.UniquePersonList; /** - * Wraps all data at the address-book level + * Wraps all data at the RecruitIntel level * Duplicates are not allowed (by .isSamePerson comparison) */ public class AddressBook implements ReadOnlyAddressBook { @@ -31,7 +32,7 @@ public class AddressBook implements ReadOnlyAddressBook { public AddressBook() {} /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} + * Creates an AddressBook using the Candidates in the {@code toBeCopied} */ public AddressBook(ReadOnlyAddressBook toBeCopied) { this(); @@ -41,15 +42,15 @@ public AddressBook(ReadOnlyAddressBook toBeCopied) { //// list overwrite operations /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. + * Replaces the contents of the candidates list with {@code candidates}. + * {@code candidates} must not contain duplicate persons. */ public void setPersons(List persons) { this.persons.setPersons(persons); } /** - * Resets the existing data of this {@code AddressBook} with {@code newData}. + * Resets the existing data of this {@code RecruitIntel} with {@code newData}. */ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); @@ -60,7 +61,7 @@ public void resetData(ReadOnlyAddressBook newData) { //// person-level operations /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a person with the same identity as {@code candidate} exists in the RecruitIntel. */ public boolean hasPerson(Person person) { requireNonNull(person); @@ -68,8 +69,8 @@ public boolean hasPerson(Person person) { } /** - * Adds a person to the address book. - * The person must not already exist in the address book. + * Adds a candidate to the RecruitIntel. + * The person must not already exist in the RecruitIntel. */ public void addPerson(Person p) { persons.add(p); @@ -77,8 +78,9 @@ public void addPerson(Person p) { /** * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * {@code target} must exist in the RecruitIntel. + * The person identity of {@code editedPerson} must not be the same as + * another existing candidate in the RecruitIntel. */ public void setPerson(Person target, Person editedPerson) { requireNonNull(editedPerson); @@ -87,13 +89,21 @@ public void setPerson(Person target, Person editedPerson) { } /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. + * Removes {@code key} from this {@code RecruitIntel}. + * {@code key} must exist in the RecruitIntel. */ public void removePerson(Person key) { persons.remove(key); } + /** + * Sorts the persons list using the given comparator. + * @param comparator the comparator to use for sorting + */ + public void sortPersons(Comparator comparator) { + persons.sort(comparator); + } + //// util methods @Override diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..536814d3a6b 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,6 +1,7 @@ package seedu.address.model; import java.nio.file.Path; +import java.util.Comparator; import java.util.function.Predicate; import javafx.collections.ObservableList; @@ -35,17 +36,17 @@ public interface Model { void setGuiSettings(GuiSettings guiSettings); /** - * Returns the user prefs' address book file path. + * Returns the user prefs' RecruitIntel file path. */ Path getAddressBookFilePath(); /** - * Sets the user prefs' address book file path. + * Sets the user prefs' RecruitIntel file path. */ void setAddressBookFilePath(Path addressBookFilePath); /** - * Replaces address book data with the data in {@code addressBook}. + * Replaces RecruitIntel data with the data in {@code addressBook}. */ void setAddressBook(ReadOnlyAddressBook addressBook); @@ -53,35 +54,44 @@ public interface Model { ReadOnlyAddressBook getAddressBook(); /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a person with the same identity as {@code person} exists in the RecruitIntel. */ boolean hasPerson(Person person); /** - * Deletes the given person. - * The person must exist in the address book. + * Deletes the given candidate. + * The person must exist in the RecruitIntel. */ void deletePerson(Person target); /** - * Adds the given person. - * {@code person} must not already exist in the address book. + * Adds the given candidate. + * {@code person} must not already exist in the RecruitIntel. */ void addPerson(Person person); /** - * Replaces the given person {@code target} with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * Replaces the given candidate {@code target} with {@code editedPerson}. + * {@code target} must exist in the RecruitIntel. + * The candidate identity of {@code editedPerson} must not be the same as + * another existing candidate in the RecruitIntel. */ void setPerson(Person target, Person editedPerson); - /** Returns an unmodifiable view of the filtered person list */ + /** Returns an unmodifiable view of the filtered candidate list */ ObservableList getFilteredPersonList(); /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. + * Updates the filter of the filtered candidate list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + void sortFilteredPersonList(Comparator comparing); + + void commit(); + + void undo() throws ModelManager.NoUndoableStateException; + + void redo() throws ModelManager.NoRedoableStateException; } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..d212edc5ead 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,9 +4,13 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; import java.util.function.Predicate; import java.util.logging.Logger; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; @@ -14,14 +18,16 @@ import seedu.address.model.person.Person; /** - * Represents the in-memory model of the address book data. + * Represents the in-memory model of the RecruitIntel data. */ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - - private final AddressBook addressBook; + private AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final ObservableList internalList = FXCollections.observableArrayList(); + private final List addressBookStateList; + private int currentStatePointer; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -29,9 +35,12 @@ public class ModelManager implements Model { public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { requireAllNonNull(addressBook, userPrefs); - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); + logger.fine("Initializing with RecruitIntel: " + addressBook + " and user prefs " + userPrefs); this.addressBook = new AddressBook(addressBook); + addressBookStateList = new ArrayList<>(); + addressBookStateList.add(new AddressBook(this.addressBook)); + currentStatePointer = 0; this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); } @@ -145,4 +154,62 @@ public boolean equals(Object other) { && filteredPersons.equals(otherModelManager.filteredPersons); } + @Override + public void sortFilteredPersonList(Comparator comparator) { + requireNonNull(comparator); + addressBook.sortPersons(comparator); + } + + + //=========== Undo/Redo ================================================================================= + /** + * Commits the current state of the address book to the history. + * This method should be called after any changes to the address book. + */ + public void commit() { + removeStatesAfterCurrentPointer(); + addressBookStateList.add(new AddressBook(this.addressBook)); + currentStatePointer++; + } + + private void removeStatesAfterCurrentPointer() { + addressBookStateList.subList(currentStatePointer + 1, addressBookStateList.size()).clear(); + } + + /** + * Reverts the model to the previous state in the history. + * + * @throws NoUndoableStateException if there is no undoable state in the model. + */ + public void undo() throws NoUndoableStateException { + if (currentStatePointer == 0) { + throw new NoUndoableStateException(); + } + currentStatePointer--; + this.setAddressBook(addressBookStateList.get(currentStatePointer)); + } + + /** + * Reverts the model to the next state in the history. + * + * @throws NoRedoableStateException if there is no redoable state in the model. + */ + public void redo() throws NoRedoableStateException { + if (currentStatePointer >= addressBookStateList.size() - 1) { + throw new NoRedoableStateException(); + } + currentStatePointer++; + this.setAddressBook(addressBookStateList.get(currentStatePointer)); + } + + /** + * Exception to indicate that there is no undoable state in the model. + */ + public static class NoUndoableStateException extends Exception {} + + /** + * Exception to indicate that there is no redoable state in the model. + */ + public static class NoRedoableStateException extends Exception {} + } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..db041a71fc7 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -4,7 +4,7 @@ import seedu.address.model.person.Person; /** - * Unmodifiable view of an address book + * Unmodifiable view of an RecruitIntel */ public interface ReadOnlyAddressBook { diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 6be655fb4c7..adbb0245aff 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -14,7 +14,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path addressBookFilePath = Paths.get("data" , "recruitintel.json"); /** * Creates a {@code UserPrefs} with default values. diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 469a2cc9a1e..e5e0bfcb45f 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -4,7 +4,7 @@ import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's address in the address book. + * Represents a Candidate's address in the RecruitIntel. * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} */ public class Address { diff --git a/src/main/java/seedu/address/model/person/Duration.java b/src/main/java/seedu/address/model/person/Duration.java new file mode 100644 index 00000000000..b11a654ee7a --- /dev/null +++ b/src/main/java/seedu/address/model/person/Duration.java @@ -0,0 +1,74 @@ +package seedu.address.model.person; + +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents the duration of an interview, in multiples of 5 minutes. + */ +public class Duration { + + public static final String MESSAGE_CONSTRAINTS = "Duration must be a positive integer multiple of 5, " + + "and cannot exceed 1440 (24 hours).\n" + + "Example: \"30\" means 30 minutes."; + + public final String value; + private final int durationInMinutes; + + /** + * Creates a Duration from a string. + * If the string is empty, it represents no duration set. + * Otherwise, the value must be a positive multiple of 5. + */ + public Duration(String duration) { + + if (duration.isBlank()) { + this.value = ""; + this.durationInMinutes = 0; // You can also use OptionalInt or -1 if you want to represent "not set" + return; + } + + checkArgument(isValidDuration(duration), MESSAGE_CONSTRAINTS); + int parsed = Integer.parseInt(duration); + this.value = duration; + this.durationInMinutes = parsed; + } + + /** + * Returns true if {@code test} is empty (meaning no duration), + * or can be parsed into a positive multiple of 5. + */ + public static boolean isValidDuration(String test) { + if (test == null || test.isBlank()) { + return true; // Accept empty as valid input + } + + try { + int durationMinutes = Integer.parseInt(test); + return durationMinutes > 0 && durationMinutes % 5 == 0 && durationMinutes <= 1440; + } catch (NumberFormatException e) { + return false; + } + } + + + @Override + public String toString() { + return value + " min"; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Duration + && value.equals(((Duration) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + public int getDurationInMinutes() { + return durationInMinutes; + } +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index c62e512bc29..57013df2bf5 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -4,7 +4,7 @@ import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's email in the address book. + * Represents a Candidate's email in the RecruitIntel. * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} */ public class Email { @@ -28,7 +28,7 @@ public class Email { 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; + 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; @@ -68,7 +68,8 @@ public boolean equals(Object other) { } Email otherEmail = (Email) other; - return value.equals(otherEmail.value); + + return setLowerCase(value).equals(setLowerCase(otherEmail.value)); } @Override @@ -76,4 +77,7 @@ public int hashCode() { return value.hashCode(); } + private String setLowerCase(String value) { + return value.toLowerCase(); + } } diff --git a/src/main/java/seedu/address/model/person/JobPosition.java b/src/main/java/seedu/address/model/person/JobPosition.java new file mode 100644 index 00000000000..2293e2e188a --- /dev/null +++ b/src/main/java/seedu/address/model/person/JobPosition.java @@ -0,0 +1,127 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; + +/** + * Represents a Candidate's job position in RecruitIntel. + * Guarantees: immutable; is valid as declared in {@link #isValidJobPosition(String)} + */ +public class JobPosition { + public static final String MESSAGE_CONSTRAINTS = + "Job position should:\n" + + "- Start with a letter or number\n" + + "- Can contain letters, numbers, spaces\n" + + "- Can contain common special characters: . , ( ) / - & + @\n" + + "- Cannot be blank"; + + /* + * Job position validation rules: + * 1. Must start with alphanumeric character: ^[\\p{Alnum}] + * 2. Can contain: + * - alphanumeric characters: \\p{Alnum} + * - spaces and special chars: [ .,()@/\\-&+] + * 3. Can have any number of these characters after the first: * + */ + public static final String VALIDATION_REGEX = "^[\\p{Alnum}][\\p{Alnum} .,()@/\\-&+]*$"; + + private static final Logger logger = LogsCenter.getLogger(JobPosition.class); + private static final String MESSAGE_EMPTY = "Job position cannot be empty.\n\n"; + private static final String MESSAGE_INVALID_START = "Job position must start " + + "with a letter or number, found: '%s'\n\n"; + private static final String MESSAGE_INVALID_CHARS = "Found invalid character(s): %s\n\n"; + private static final String ALLOWED_SPECIAL_CHARS = ". ,()@/-&+ "; + + public final String value; + + /** + * Constructs a {@code JobPosition}. + * + * @param jobPosition A valid job position. + */ + public JobPosition(String jobPosition) { + requireNonNull(jobPosition); + String validationError = getValidationErrorMessage(jobPosition); + if (!validationError.equals(MESSAGE_CONSTRAINTS)) { + logger.warning("Invalid job position attempted: " + jobPosition); + throw new IllegalArgumentException(validationError); + } + value = jobPosition; + } + + /** + * Returns true if a given string is a valid job position. + */ + public static boolean isValidJobPosition(String test) { + try { + new JobPosition(test); + return true; + } catch (IllegalArgumentException e) { + logger.fine("Job position validation failed: " + test); + return false; + } + } + + /** + * Returns a specific error message identifying which characters are invalid in the job position. + */ + private static String getValidationErrorMessage(String jobPosition) { + if (jobPosition.isEmpty()) { + logger.fine("Empty job position detected"); + return MESSAGE_EMPTY + MESSAGE_CONSTRAINTS; + } + + if (!Character.isLetterOrDigit(jobPosition.charAt(0))) { + logger.fine("Invalid starting character in job position: " + jobPosition.charAt(0)); + return String.format(MESSAGE_INVALID_START, jobPosition.charAt(0)) + MESSAGE_CONSTRAINTS; + } + + String invalidChars = findInvalidCharacters(jobPosition); + if (!invalidChars.isEmpty()) { + logger.fine("Invalid characters found in job position: " + invalidChars); + return String.format(MESSAGE_INVALID_CHARS, invalidChars) + MESSAGE_CONSTRAINTS; + } + + return MESSAGE_CONSTRAINTS; + } + + /** + * Returns a string containing all invalid characters found in the job position. + */ + private static String findInvalidCharacters(String jobPosition) { + StringBuilder invalidChars = new StringBuilder(); + for (char c : jobPosition.toCharArray()) { + if (!Character.isLetterOrDigit(c) && !ALLOWED_SPECIAL_CHARS.contains(String.valueOf(c))) { + invalidChars.append("'").append(c).append("' "); + } + } + return invalidChars.toString().trim(); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof JobPosition)) { + return false; + } + + JobPosition otherJobPosition = (JobPosition) other; + return value.equals(otherJobPosition.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/JobPositionContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/JobPositionContainsKeywordsPredicate.java new file mode 100644 index 00000000000..2c32a84d81a --- /dev/null +++ b/src/main/java/seedu/address/model/person/JobPositionContainsKeywordsPredicate.java @@ -0,0 +1,51 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.function.Predicate; + +/** + * Tests that a {@code Person}'s {@code JobPosition} matches any of the keywords given. + * Keywords are matched case-insensitively against any part of the job position. + */ +public class JobPositionContainsKeywordsPredicate implements Predicate { + private final List keywords; + + /** + * Constructs a new {@code JobPositionContainsKeywordsPredicate}. + * @param keywords the list of keywords to match against job positions + */ + public JobPositionContainsKeywordsPredicate(List keywords) { + requireNonNull(keywords); + keywords.forEach(keyword -> requireNonNull(keyword, "Keywords cannot contain null elements")); + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + requireNonNull(person); + assert person.getJobPosition() != null : "Person's job position cannot be null"; + + String jobPositionText = person.getJobPosition().toString().toLowerCase(); + assert !jobPositionText.isEmpty() : "Job position text cannot be empty after conversion"; + + return keywords.stream() + .map(String::toLowerCase) + .anyMatch(jobPositionText::contains); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof JobPositionContainsKeywordsPredicate)) { + return false; + } + + JobPositionContainsKeywordsPredicate otherPredicate = (JobPositionContainsKeywordsPredicate) other; + return keywords.equals(otherPredicate.keywords); + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..26d4f82056f 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -4,19 +4,28 @@ import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's name in the address book. + * Represents a Candidate's name in the RecruitIntel. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ 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:\n" + + "1. Start with a letter or number\n" + + "2. Can contain letters (including accented letters), numbers, spaces\n" + + "3. Can contain hyphens (-), apostrophes ('), periods (.), and forward slashes (/)\n" + + "4. Cannot be blank"; /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. + * Name validation rules: + * 1. Must start with alphanumeric: ^[\\p{Alnum}] + * 2. Can contain: + * - alphanumeric characters (including accented letters): \\p{Alnum} + * - spaces: \\s + * - hyphens, apostrophes, periods, forward slashes: [\\-\\'\\.\\/ ] + * 3. Can have any number of these characters after the first: * */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "^[\\p{Alnum}\\p{L}][\\p{L}\\p{Alnum}\\s\\-\\'\\.\\/ ]*"; public final String fullName; diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java index 62d19be2977..65cc8f3f9de 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java @@ -3,11 +3,10 @@ import java.util.List; import java.util.function.Predicate; -import seedu.address.commons.util.StringUtil; import seedu.address.commons.util.ToStringBuilder; /** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + * Tests that a {@code Candidate}'s {@code Name} matches any of the keywords given. */ public class NameContainsKeywordsPredicate implements Predicate { private final List keywords; @@ -19,7 +18,7 @@ public NameContainsKeywordsPredicate(List keywords) { @Override public boolean test(Person person) { return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + .anyMatch(keyword -> person.getName().fullName.toLowerCase().contains(keyword.toLowerCase())); } @Override diff --git a/src/main/java/seedu/address/model/person/Notes.java b/src/main/java/seedu/address/model/person/Notes.java new file mode 100644 index 00000000000..630a39b4b51 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Notes.java @@ -0,0 +1,71 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents interviewer notes for a Candidate in RecruitIntel. + * Guarantees: immutable; is valid as declared in {@link #isValidNotes(String)} + */ +public class Notes { + + public static final int MAX_LENGTH = 450; + public static final String MESSAGE_CONSTRAINTS = + String.format("Notes cannot exceed %d characters in length.", MAX_LENGTH); + + /* + * Notes validation rules: + * 1. Can be any string (including empty) + * 2. Must not exceed MAX_LENGTH characters + * 3. Can contain any characters (multiline allowed) + * + * The regex below uses a non-greedy match (.*?) to match any character + * up to MAX_LENGTH times + */ + private static final String VALIDATION_REGEX = String.format("^.{0,%d}$", MAX_LENGTH); + + public final String value; + + /** + * Constructs a {@code Notes}. + * + * @param notes A valid notes string. + */ + public Notes(String notes) { + requireNonNull(notes); + checkArgument(isValidNotes(notes), MESSAGE_CONSTRAINTS); + value = notes; + } + + /** + * Returns true if a given string is valid for notes. + * A valid notes string must not exceed MAX_LENGTH characters. + */ + public static boolean isValidNotes(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Notes)) { + return false; + } + + Notes otherNotes = (Notes) other; + return value.equals(otherNotes.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..e262238b5f9 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -11,46 +11,79 @@ import seedu.address.model.tag.Tag; /** - * Represents a Person in the address book. + * Represents a Candidate in RecruitIntel. * Guarantees: details are present and not null, field values are validated, immutable. */ public class Person { - // Identity fields + // Identity fields - used to uniquely identify a candidate private final Name name; - private final Phone phone; private final Email email; + private final Phone phone; - // Data fields - private final Address address; + // Professional fields - related to job application + private final JobPosition jobPosition; + private final Team team; private final Set tags = new HashSet<>(); + // Interview fields - related to interview scheduling + private final StartTime startTime; + private final Duration duration; + + // Additional fields + private final Address address; + private final Notes notes; + + /** + * Creates a Person with default empty values for optional fields. + * Every field must be present and not null. + */ + public Person(Name name, Phone phone, Email email, Address address, JobPosition jobPosition, + Team team, Set tags) { + this(name, phone, email, address, jobPosition, team, tags, + new Notes(""), new StartTime(""), new Duration("")); + } + /** + * Creates a Person with all fields specified. * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Phone phone, Email email, Address address, JobPosition jobPosition, + Team team, Set tags, Notes notes, StartTime startTime, Duration duration) { + requireAllNonNull(name, phone, email, address, jobPosition, team, tags, notes, startTime, duration); + this.name = name; this.phone = phone; this.email = email; this.address = address; + this.jobPosition = jobPosition; + this.team = team; this.tags.addAll(tags); + this.startTime = startTime; + this.duration = duration; + this.notes = notes; } + // Identity field accessors public Name getName() { return name; } + public Email getEmail() { + return email; + } + public Phone getPhone() { return phone; } - public Email getEmail() { - return email; + // Professional field accessors + public JobPosition getJobPosition() { + return jobPosition; } - public Address getAddress() { - return address; + public Team getTeam() { + return team; } /** @@ -61,9 +94,27 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + // Interview field accessors + public StartTime getStartTime() { + return startTime; + } + + public Duration getDuration() { + return duration; + } + + // Additional field accessors + public Address getAddress() { + return address; + } + + public Notes getNotes() { + return notes; + } + /** - * Returns true if both persons have the same name. - * This defines a weaker notion of equality between two persons. + * Returns true if both candidates have the same email. + * This defines a weaker notion of equality between two candidates. */ public boolean isSamePerson(Person otherPerson) { if (otherPerson == this) { @@ -71,12 +122,12 @@ public boolean isSamePerson(Person otherPerson) { } return otherPerson != null - && otherPerson.getName().equals(getName()); + && otherPerson.getEmail().equals(getEmail()); } /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. + * Returns true if both candidates have the same identity and data fields. + * This defines a stronger notion of equality between two candidates. */ @Override public boolean equals(Object other) { @@ -94,13 +145,18 @@ public boolean equals(Object other) { && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) - && tags.equals(otherPerson.tags); + && jobPosition.equals(otherPerson.jobPosition) + && team.equals(otherPerson.team) + && tags.equals(otherPerson.tags) + && startTime.equals(otherPerson.startTime) + && duration.equals(otherPerson.duration) + && notes.equals(otherPerson.notes); } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, address, jobPosition, team, tags, notes, startTime, duration); } @Override @@ -110,8 +166,12 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("jobPosition", jobPosition) + .add("team", team) .add("tags", tags) + .add("notes", notes) + .add("interview time", startTime) + .add("duration", duration) .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..80b6b4bf3c3 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -4,14 +4,14 @@ import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's phone number in the address book. + * Represents a Candidate's phone number in the RecruitIntel. * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} */ public class Phone { public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; + "Phone numbers should only contain numbers. It should not be blank and at least 3 digits long"; public static final String VALIDATION_REGEX = "\\d{3,}"; public final String value; diff --git a/src/main/java/seedu/address/model/person/StartTime.java b/src/main/java/seedu/address/model/person/StartTime.java new file mode 100644 index 00000000000..27596a689a2 --- /dev/null +++ b/src/main/java/seedu/address/model/person/StartTime.java @@ -0,0 +1,135 @@ +package seedu.address.model.person; + +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Represents the start time of an interview in the format "yyyy-MM-dd HH:mm". + */ +public class StartTime implements Comparable { + + public static final String MESSAGE_CONSTRAINTS = "Start time must follow the \"yyyy-MM-dd HH:mm\" format, " + + "and the minutes must be in multiples of 5 (e.g., 10, 15, 20).\n" + + "Example: \"2025-04-01 10:15\""; + + public static final String VALIDATION_REGEX = "^$|^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$"; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + public final String value; + private final LocalDateTime parsedStartTime; + + /** + * Constructs a {@code StartTime} from the given string. + * If input is null or blank, no value is parsed. + * + * @param startTime A string in "yy-MM-dd HH-mm" format or null + */ + public StartTime(String startTime) { + if (startTime == null || startTime.isBlank()) { + this.value = ""; + this.parsedStartTime = null; + return; + } + + checkArgument(isValidStartTime(startTime), MESSAGE_CONSTRAINTS); + + this.value = startTime; + this.parsedStartTime = LocalDateTime.parse(startTime, FORMATTER); + } + + + /** + * Returns true if {@code test} is a valid StartTime string or null. + */ + public static boolean isValidStartTime(String test) { + if (test == null || test.isBlank()) { + return true; + } + + if (!test.matches(VALIDATION_REGEX)) { + return false; + } + + try { + LocalDateTime parsed = LocalDateTime.parse(test, FORMATTER); + + int year = parsed.getYear(); + int month = parsed.getMonthValue(); + int day = Integer.parseInt(test.substring(8, 10)); + + if (day > getDaysInMonth(year, month)) { + return false; + } + + return isMinuteMultipleOfFive(parsed); + } catch (DateTimeParseException e) { + return false; + } + } + + private static int getDaysInMonth(int year, int month) { + switch (month) { + case 1: // January + case 3: // March + case 5: // May + case 7: // July + case 8: // August + case 10: // October + case 12: // December + return 31; + case 4: // April + case 6: // June + case 9: // September + case 11: // November + return 30; + default: // February + return isLeapYear(year) ? 29 : 28; + } + } + + private static boolean isLeapYear(int year) { + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); + } + + public static boolean isMinuteMultipleOfFive(LocalDateTime dateTime) { + return dateTime.getMinute() % 5 == 0; + } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof StartTime + && value.equals(((StartTime) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + public LocalDateTime getParsedStartTime() { + return this.parsedStartTime; + } + + @Override + public int compareTo(StartTime other) { + if (this.parsedStartTime == null && other.parsedStartTime == null) { + return 0; + } + if (this.parsedStartTime == null) { + return 1; // treat nulls as later + } + if (other.parsedStartTime == null) { + return -1; + } + return this.parsedStartTime.compareTo(other.parsedStartTime); + } + +} diff --git a/src/main/java/seedu/address/model/person/TagsContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/TagsContainsKeywordsPredicate.java new file mode 100644 index 00000000000..817847b6744 --- /dev/null +++ b/src/main/java/seedu/address/model/person/TagsContainsKeywordsPredicate.java @@ -0,0 +1,46 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Candidate}'s any of the {@code Tag} matches any of the keywords given. + */ +public class TagsContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public TagsContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> + person.getTags().stream() + .anyMatch(tag -> tag.getTagName().toLowerCase().contains(keyword.toLowerCase())) + ); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof TagsContainsKeywordsPredicate)) { + return false; + } + + TagsContainsKeywordsPredicate otherTagsContainsKeywordsPredicate = (TagsContainsKeywordsPredicate) other; + return keywords.equals(otherTagsContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/Team.java b/src/main/java/seedu/address/model/person/Team.java new file mode 100644 index 00000000000..3e1afadb65a --- /dev/null +++ b/src/main/java/seedu/address/model/person/Team.java @@ -0,0 +1,126 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; + +/** + * Represents a Candidate's team in RecruitIntel. + * Guarantees: immutable; is valid as declared in {@link #isValidTeam(String)} + */ +public class Team { + public static final String MESSAGE_CONSTRAINTS = + "Team name should:\n" + + "- Start with a letter or number\n" + + "- Can contain letters, numbers, spaces\n" + + "- Can contain common special characters: . , ( ) / - & + @\n" + + "- Cannot be blank"; + + /* + * Team name validation rules: + * 1. Must start with alphanumeric character: ^[\\p{Alnum}] + * 2. Can contain: + * - alphanumeric characters: \\p{Alnum} + * - spaces and special chars: [ .,()@/\\-&+] + * 3. Can have any number of these characters after the first: * + */ + public static final String VALIDATION_REGEX = "^[\\p{Alnum}][\\p{Alnum} .,()@/\\-&+]*$"; + + private static final Logger logger = LogsCenter.getLogger(Team.class); + private static final String MESSAGE_EMPTY = "Team name cannot be empty.\n\n"; + private static final String MESSAGE_INVALID_START = "Team name must start with a letter or number, found: '%s'\n\n"; + private static final String MESSAGE_INVALID_CHARS = "Found invalid character(s): %s\n\n"; + private static final String ALLOWED_SPECIAL_CHARS = ". ,()@/-&+ "; + + public final String value; + + /** + * Constructs a {@code Team}. + * + * @param team A valid team. + */ + public Team(String team) { + requireNonNull(team); + String validationError = getValidationErrorMessage(team); + if (!validationError.equals(MESSAGE_CONSTRAINTS)) { + logger.warning("Invalid team name attempted: " + team); + throw new IllegalArgumentException(validationError); + } + value = team; + } + + /** + * Returns true if a given string is a valid team. + */ + public static boolean isValidTeam(String test) { + try { + new Team(test); + return true; + } catch (IllegalArgumentException e) { + logger.fine("Team name validation failed: " + test); + return false; + } + } + + /** + * Returns a specific error message identifying which characters are invalid in the team name. + */ + private static String getValidationErrorMessage(String team) { + if (team.isEmpty()) { + logger.fine("Empty team name detected"); + return MESSAGE_EMPTY + MESSAGE_CONSTRAINTS; + } + + if (!Character.isLetterOrDigit(team.charAt(0))) { + logger.fine("Invalid starting character in team name: " + team.charAt(0)); + return String.format(MESSAGE_INVALID_START, team.charAt(0)) + MESSAGE_CONSTRAINTS; + } + + String invalidChars = findInvalidCharacters(team); + if (!invalidChars.isEmpty()) { + logger.fine("Invalid characters found in team name: " + invalidChars); + return String.format(MESSAGE_INVALID_CHARS, invalidChars) + MESSAGE_CONSTRAINTS; + } + + return MESSAGE_CONSTRAINTS; + } + + /** + * Returns a string containing all invalid characters found in the team name. + */ + private static String findInvalidCharacters(String team) { + StringBuilder invalidChars = new StringBuilder(); + for (char c : team.toCharArray()) { + if (!Character.isLetterOrDigit(c) && !ALLOWED_SPECIAL_CHARS.contains(String.valueOf(c))) { + invalidChars.append("'").append(c).append("' "); + } + } + return invalidChars.toString().trim(); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Team)) { + return false; + } + + Team otherTeam = (Team) other; + return value.equals(otherTeam.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/TeamContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/TeamContainsKeywordsPredicate.java new file mode 100644 index 00000000000..4461106f190 --- /dev/null +++ b/src/main/java/seedu/address/model/person/TeamContainsKeywordsPredicate.java @@ -0,0 +1,51 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.function.Predicate; + +/** + * Tests that a {@code Person}'s {@code Team} matches any of the keywords given. + * Keywords are matched case-insensitively against any part of the team name. + */ +public class TeamContainsKeywordsPredicate implements Predicate { + private final List keywords; + + /** + * Constructs a new {@code TeamContainsKeywordsPredicate}. + * @param keywords the list of keywords to match against team names + */ + public TeamContainsKeywordsPredicate(List keywords) { + requireNonNull(keywords); + keywords.forEach(keyword -> requireNonNull(keyword, "Keywords cannot contain null elements")); + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + requireNonNull(person); + assert person.getTeam() != null : "Person's team cannot be null"; + + String teamText = person.getTeam().toString().toLowerCase(); + assert !teamText.isEmpty() : "Team text cannot be empty after conversion"; + + return keywords.stream() + .map(String::toLowerCase) + .anyMatch(teamText::contains); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof TeamContainsKeywordsPredicate)) { + return false; + } + + TeamContainsKeywordsPredicate otherPredicate = (TeamContainsKeywordsPredicate) other; + return keywords.equals(otherPredicate.keywords); + } +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index cc0a68d79f9..e447f5da08e 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -3,6 +3,7 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -36,15 +37,19 @@ public boolean contains(Person toCheck) { return internalList.stream().anyMatch(toCheck::isSamePerson); } + /** * Adds a person to the list. * The person must not already exist in the list. */ public void add(Person toAdd) { requireNonNull(toAdd); + if (contains(toAdd)) { throw new DuplicatePersonException(); } + + internalList.add(toAdd); } @@ -134,6 +139,16 @@ public String toString() { return internalList.toString(); } + /** + * Sorts the internal list using the given comparator. + * @param comparator the comparator to use for sorting + */ + public void sort(Comparator comparator) { + requireNonNull(comparator); + FXCollections.sort(internalList, comparator); + } + + /** * Returns true if {@code persons} contains only unique persons. */ diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java index d7290f59442..d11cef54648 100644 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ b/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java @@ -1,11 +1,11 @@ package seedu.address.model.person.exceptions; /** - * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same - * identity). + * Signals that the operation will result in duplicate Candidates (Candidates + * are considered duplicates if they have the same identity). */ public class DuplicatePersonException extends RuntimeException { public DuplicatePersonException() { - super("Operation would result in duplicate persons"); + super("Operation would result in duplicate candidates"); } } diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java index fa764426ca7..bbae460f8b7 100644 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java @@ -1,6 +1,6 @@ package seedu.address.model.person.exceptions; /** - * Signals that the operation is unable to find the specified person. + * Signals that the operation is unable to find the specified candidate. */ public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index f1a0d4e233b..88e3fbf74c1 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -4,13 +4,15 @@ import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Tag in the address book. + * Represents a Tag in the RecruitIntel. * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} */ public class Tag { - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + public static final String MESSAGE_CONSTRAINTS = + "Tags should only contain alphanumeric characters, dots, plus signs, hash symbols, and hyphens. " + + "It must not be blank and it cannot contain spaces."; + public static final String VALIDATION_REGEX = "^[\\p{Alnum}+#.\\-]+$"; public final String tagName; @@ -25,6 +27,10 @@ public Tag(String tagName) { this.tagName = tagName; } + public String getTagName() { + return tagName; + } + /** * Returns true if a given string is a valid tag name. */ diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..f5439f21cec 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -7,10 +7,15 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Address; +import seedu.address.model.person.Duration; import seedu.address.model.person.Email; +import seedu.address.model.person.JobPosition; import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.StartTime; +import seedu.address.model.person.Team; import seedu.address.model.tag.Tag; /** @@ -20,23 +25,110 @@ 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 Address("Blk 30 Geylang Street 29, #06-40"), + new JobPosition("Software Engineer"), new Team("iOS Development"), + getTagSet("swift", "senior", "mobile"), + new Notes("Strong in iOS development with 8 years of experience. Led multiple successful App Store " + + "releases. Expert in Swift, SwiftUI, and iOS architecture. Mentors junior developers and " + + "contributes to architectural decisions. " + + "Excellent problem-solving skills and attention to detail."), + new StartTime("2025-05-01 12:00"), new Duration("90")), 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 Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), + new JobPosition("UI/UX Designer"), new Team("Design"), + getTagSet("figma", "experienced", "design"), + new Notes("Expert in user interface design with strong portfolio. Proficient in Figma and Adobe " + + "Creative Suite. Has redesigned multiple " + + "high-traffic applications improving user satisfaction " + + "by 40%. Strong advocate for accessibility and inclusive design principles."), + new StartTime("2025-05-10 12:00"), new Duration("90")), + new Person(new Name("Charlie Tan"), new Phone("93210283"), new Email("charlie@example.com"), + new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), + new JobPosition("Software Engineer"), new Team("Android Development"), + getTagSet("kotlin", "junior", "mobile"), + new Notes("Passionate about Android development with 2 years of experience. Strong foundation in " + + "Kotlin and Android SDK. Quick learner who has already contributed to several feature " + + "releases. Particularly interested in app performance optimization."), + new StartTime("2025-03-22 14:15"), new Duration("60")), 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 Address("Blk 436 Serangoon Gardens Street 26, #16-43"), + new JobPosition("Security Engineer"), new Team("Security"), + getTagSet("security", "senior", "cloud"), + new Notes("Expert in cloud security and penetration testing with 10 years of experience. CISSP " + + "certified. Led security audits for major cloud platforms. Strong background in threat " + + "modeling and security architecture. Regular speaker at security conferences."), + new StartTime("2025-04-01 12:00"), new Duration("60")), + new Person(new Name("Emma Wong"), new Phone("92492021"), new Email("emma@example.com"), + new Address("Blk 47 Tampines Street 20, #17-35"), + new JobPosition("Software Engineer"), new Team("Backend"), + getTagSet("java", "spring", "experienced"), + new Notes("Specializes in Java backend development with 5 years of experience. " + + "Expert in Spring Boot " + + "and microservices architecture. Has successfully led the migration of monolithic " + + "applications to microservices. Strong advocate for clean code " + + "and test-driven development."), + new StartTime("2025-04-05 12:00"), new Duration("45")), + new Person(new Name("Fiona Tan"), new Phone("92624417"), new Email("fiona@example.com"), + new Address("Blk 45 Aljunied Street 85, #11-31"), + new JobPosition("Product Manager"), new Team("iOS Development"), + getTagSet("agile", "experienced", "mobile"), + new Notes("Strong track record in mobile app product management with 6 years of experience. " + + "Successfully launched multiple iOS apps with millions of downloads. " + + "Expert in agile methodologies and " + + "user-centered design. Great at stakeholder management."), + new StartTime("2025-03-30 12:00"), new Duration("90")), + new Person(new Name("George Zhang"), new Phone("92624418"), new Email("george@example.com"), + new Address("Blk 123 Clementi Street 11, #12-21"), + new JobPosition("Data Scientist"), new Team("Data"), + getTagSet("python", "ml", "experienced"), + new Notes("Expert in machine learning and data analytics with 5 years of experience. " + + "PhD in Computer Science. " + + "Specializes in natural language processing and computer vision. Has published " + + "several papers in top ML conferences. Strong Python and TensorFlow skills."), + new StartTime("2025-04-10 14:00"), new Duration("60")), + new Person(new Name("Hannah Lee"), new Phone("92624419"), new Email("hannah@example.com"), + new Address("Blk 789 Yishun Ring Road, #05-12"), + new JobPosition("UI/UX Designer"), new Team("Design"), + getTagSet("design", "junior", "figma"), + new Notes("Creative designer with fresh perspective and 1 year of experience. Strong portfolio of " + + "mobile and web designs. " + + "Proficient in Figma and prototyping tools. Passionate about user " + + "research and has conducted several successful usability studies."), + new StartTime("2025-04-15 10:00"), new Duration("45")), + new Person(new Name("Ian Lim"), new Phone("92624420"), new Email("ian@example.com"), + new Address("Blk 456 Hougang Ave 10, #08-88"), + new JobPosition("DevOps Engineer"), new Team("Infrastructure"), + getTagSet("kubernetes", "aws", "senior"), + new Notes("Experienced in cloud infrastructure and CI/CD with 9 years of experience. " + + "Expert in AWS, Kubernetes, and Terraform. " + + "Has successfully led multiple cloud migrations. Strong advocate " + + "for infrastructure as code and automated testing. AWS certified solutions architect."), + new StartTime("2025-04-20 11:30"), new Duration("60")), + new Person(new Name("Julia Chen"), new Phone("92624421"), new Email("julia@example.com"), + new Address("Blk 789 Jurong East Street 42, #15-33"), + new JobPosition("Software Engineer"), new Team("Backend"), + getTagSet("python", "django", "junior"), + new Notes("Strong foundation in Python backend development with 2 years of experience. " + + "Proficient in Django and REST APIs. " + + "Quick learner who has already taken ownership of several key " + + "features. Passionate about writing clean, maintainable code and documentation."), + new StartTime("2025-04-25 15:00"), new Duration("45")), + new Person(new Name("Kevin Patel"), new Phone("92624422"), new Email("kevin@example.com"), + new Address("Blk 147 Bishan Street 13, #09-77"), + new JobPosition("Product Manager"), new Team("Android Development"), + getTagSet("agile", "mobile", "senior"), + new Notes("Experienced in Android app product strategy with 8 years of experience. Has launched " + + "multiple successful apps with over 10M+ downloads. Strong understanding of the Android " + + "ecosystem and Material Design. Expert in data-driven decision making and A/B testing."), + new StartTime("2025-05-01 13:00"), new Duration("90")), + new Person(new Name("Linda Kim"), new Phone("92624423"), new Email("linda@example.com"), + new Address("Blk 258 Pasir Ris Street 21, #14-55"), + new JobPosition("Software Engineer"), new Team("iOS Development"), + getTagSet("swift", "junior", "mobile"), + new Notes("Enthusiastic about iOS development with 1.5 years of experience. Strong foundation in " + + "Swift and UIKit. Fast learner who has already contributed to several feature releases. " + + "Passionate about mobile UI animations and smooth user experiences."), + new StartTime("2025-05-05 16:00"), new Duration("60")) }; } @@ -56,5 +148,4 @@ public static Set getTagSet(String... strings) { .map(Tag::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..c8a81b89d7a 100644 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ b/src/main/java/seedu/address/storage/AddressBookStorage.java @@ -18,7 +18,7 @@ public interface AddressBookStorage { Path getAddressBookFilePath(); /** - * Returns AddressBook data as a {@link ReadOnlyAddressBook}. + * Returns RecruitIntel data as a {@link ReadOnlyAddressBook}. * Returns {@code Optional.empty()} if storage file is not found. * * @throws DataLoadingException if loading the data from storage failed. diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..b66d3f87865 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -11,10 +11,15 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.person.Address; +import seedu.address.model.person.Duration; import seedu.address.model.person.Email; +import seedu.address.model.person.JobPosition; import seedu.address.model.person.Name; +import seedu.address.model.person.Notes; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.StartTime; +import seedu.address.model.person.Team; import seedu.address.model.tag.Tag; /** @@ -22,28 +27,41 @@ */ class JsonAdaptedPerson { - public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Candidate's %s field is missing!"; private final String name; private final String phone; private final String email; private final String address; + private final String jobPosition; + private final String team; + private final String notes; private final List tags = new ArrayList<>(); + private final String startTime; + private final String duration; /** * Constructs a {@code JsonAdaptedPerson} with the given person details. */ @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("email") String email, @JsonProperty("address") String address, + @JsonProperty("jobPosition") String jobPosition, @JsonProperty("team") String team, + @JsonProperty("notes") String notes, @JsonProperty("tags") List tags, + @JsonProperty("startTime") String startTime, @JsonProperty("duration") String duration) { + this.name = name; this.phone = phone; this.email = email; this.address = address; + this.jobPosition = jobPosition; + this.team = team; + this.notes = notes; if (tags != null) { this.tags.addAll(tags); } + this.startTime = startTime; + this.duration = duration; } /** @@ -54,9 +72,14 @@ public JsonAdaptedPerson(Person source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; + jobPosition = source.getJobPosition().value; + team = source.getTeam().value; + notes = source.getNotes().value; tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + startTime = source.getStartTime().value; + duration = source.getDuration().value; } /** @@ -65,45 +88,133 @@ public JsonAdaptedPerson(Person source) { * @throws IllegalValueException if there were any data constraints violated in the adapted person. */ public Person toModelType() throws IllegalValueException { + final List personTags = convertTags(); + + final Name modelName = validateAndCreateName(); + final Phone modelPhone = validateAndCreatePhone(); + final Email modelEmail = validateAndCreateEmail(); + final Address modelAddress = validateAndCreateAddress(); + final JobPosition modelJobPosition = validateAndCreateJobPosition(); + final Team modelTeam = validateAndCreateTeam(); + + // Optional fields with default empty values + final StartTime modelStartTime = validateAndCreateStartTime(); + final Duration modelDuration = validateAndCreateDuration(); + final Notes modelNotes = new Notes(notes != null ? notes : ""); + final Set modelTags = new HashSet<>(personTags); + + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelJobPosition, + modelTeam, modelTags, modelNotes, modelStartTime, modelDuration); + } + + /** + * Converts the stored tags into a List of Tag objects. + */ + private List convertTags() throws IllegalValueException { final List personTags = new ArrayList<>(); for (JsonAdaptedTag tag : tags) { personTags.add(tag.toModelType()); } + return personTags; + } + /** + * Validates and creates a Name object. + */ + private Name validateAndCreateName() throws IllegalValueException { if (name == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); } if (!Name.isValidName(name)) { throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); } - final Name modelName = new Name(name); + return new Name(name); + } + /** + * Validates and creates a Phone object. + */ + private Phone validateAndCreatePhone() throws IllegalValueException { if (phone == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); } if (!Phone.isValidPhone(phone)) { throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); } - final Phone modelPhone = new Phone(phone); + return new Phone(phone); + } + /** + * Validates and creates an Email object. + */ + private Email validateAndCreateEmail() throws IllegalValueException { if (email == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); } if (!Email.isValidEmail(email)) { throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); } - final Email modelEmail = new Email(email); + return new Email(email); + } + /** + * Validates and creates an Address object. + */ + private Address validateAndCreateAddress() throws IllegalValueException { 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); + return new Address(address); + } - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + /** + * Validates and creates a JobPosition object. + */ + private JobPosition validateAndCreateJobPosition() throws IllegalValueException { + if (jobPosition == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + JobPosition.class.getSimpleName())); + } + if (!JobPosition.isValidJobPosition(jobPosition)) { + throw new IllegalValueException(JobPosition.MESSAGE_CONSTRAINTS); + } + return new JobPosition(jobPosition); + } + + /** + * Validates and creates a Team object. + */ + private Team validateAndCreateTeam() throws IllegalValueException { + if (team == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Team.class.getSimpleName())); + } + if (!Team.isValidTeam(team)) { + throw new IllegalValueException(Team.MESSAGE_CONSTRAINTS); + } + return new Team(team); + } + + private StartTime validateAndCreateStartTime() throws IllegalValueException { + if (startTime == null) { + return new StartTime(""); + } + if (!StartTime.isValidStartTime(startTime)) { + throw new IllegalValueException(StartTime.MESSAGE_CONSTRAINTS); + } + return new StartTime(startTime); } + private Duration validateAndCreateDuration() throws IllegalValueException { + if (duration == null) { + return new Duration(""); + } + if (!Duration.isValidDuration(duration)) { + throw new IllegalValueException(Duration.MESSAGE_CONSTRAINTS); + } + return new Duration(duration); + } } diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java index 41e06f264e1..dd7ca9ce21f 100644 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java @@ -15,7 +15,7 @@ import seedu.address.model.ReadOnlyAddressBook; /** - * A class to access AddressBook data stored as a json file on the hard disk. + * A class to access RecruitIntel data stored as a json file on the hard disk. */ public class JsonAddressBookStorage implements AddressBookStorage { diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..b0c76dbb9c1 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -16,7 +16,7 @@ /** * An Immutable AddressBook that is serializable to JSON format. */ -@JsonRootName(value = "addressbook") +@JsonRootName(value = "recruitintel") class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; @@ -41,7 +41,7 @@ public JsonSerializableAddressBook(ReadOnlyAddressBook source) { } /** - * Converts this address book into the model's {@code AddressBook} object. + * Converts this RecruitIntel into the model's {@code AddressBook} object. * * @throws IllegalValueException if there were any data constraints violated. */ diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index 8b84a9024d5..1ed4f01bde5 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -12,7 +12,7 @@ import seedu.address.model.UserPrefs; /** - * Manages storage of AddressBook data in local storage. + * Manages storage of RecruitIntel data in local storage. */ public class StorageManager implements Storage { @@ -21,7 +21,7 @@ public class StorageManager implements Storage { private UserPrefsStorage userPrefsStorage; /** - * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}. + * Creates a {@code StorageManager} with the given {@code RecruitIntelStorage} and {@code UserPrefStorage}. */ public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { this.addressBookStorage = addressBookStorage; @@ -46,7 +46,7 @@ public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { } - // ================ AddressBook methods ============================== + // ================ RecruitIntel methods ============================== @Override public Path getAddressBookFilePath() { diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..5976c6aacde 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -11,11 +11,11 @@ import seedu.address.commons.core.LogsCenter; /** - * Controller for a help page + * Controller for a help page that displays the user guide URL and allows copying it to clipboard. */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + private static final String USERGUIDE_URL = "https://ay2425s2-cs2103t-f14-3.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..ca08d52ff8b 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -1,20 +1,32 @@ package seedu.address.ui; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Comparator; +import java.util.logging.Logger; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import seedu.address.commons.core.LogsCenter; import seedu.address.model.person.Person; /** * An UI component that displays information of a {@code Person}. */ public class PersonCard extends UiPart { + private static final Logger logger = LogsCenter.getLogger(PersonCard.class); private static final String FXML = "PersonListCard.fxml"; + private static final String INPUT_DATE_FORMAT = "yyyy-MM-dd HH:mm"; + private static final String DISPLAY_DATE_FORMAT = "d MMM yyyy, h:mm a"; + private static final String MINUTES_SUFFIX = " minutes"; + private static final DateTimeFormatter INPUT_FORMATTER = DateTimeFormatter.ofPattern(INPUT_DATE_FORMAT); + private static final DateTimeFormatter DISPLAY_FORMATTER = DateTimeFormatter.ofPattern(DISPLAY_DATE_FORMAT); /** * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. @@ -33,27 +45,118 @@ public class PersonCard extends UiPart { @FXML private Label id; @FXML + private Label phoneLabel; + @FXML private Label phone; @FXML + private Label addressLabel; + @FXML private Label address; @FXML + private Label emailLabel; + @FXML private Label email; @FXML + private Label jobPositionLabel; + @FXML + private Label jobPosition; + @FXML + private Label teamLabel; + @FXML + private Label team; + @FXML + private Label startTimeLabel; + @FXML + private Label startTime; + @FXML + private Label durationLabel; + @FXML + private Label duration; + @FXML private FlowPane tags; + @FXML + private Label interviewerNotes; + @FXML + private VBox interviewDetailsBox; /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. */ public PersonCard(Person person, int displayedIndex) { super(FXML); + if (person == null) { + logger.warning("Attempt to create PersonCard with null person"); + throw new IllegalArgumentException("Person cannot be null"); + } + if (displayedIndex < 0) { + logger.warning("Attempt to create PersonCard with invalid index: " + displayedIndex); + throw new IllegalArgumentException("Display index must be non-negative"); + } + this.person = person; + logger.fine("Creating PersonCard for " + person.getName().fullName + " at index " + displayedIndex); + + try { + initializeBasicInfo(displayedIndex); + initializeTags(); + setupInterviewDetails(); + } catch (Exception e) { + logger.warning("Error initializing PersonCard: " + e.getMessage()); + throw new IllegalStateException("Failed to initialize PersonCard", e); + } + } + + /** + * Initializes the basic information fields of the person card. + */ + private void initializeBasicInfo(int displayedIndex) { id.setText(displayedIndex + ". "); name.setText(person.getName().fullName); phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); + jobPosition.setText(person.getJobPosition().value); + team.setText(person.getTeam().value); + interviewerNotes.setText(person.getNotes().value); + logger.fine("Basic information initialized for " + person.getName().fullName); + } + + /** + * Initializes the tags section of the person card. + */ + private void initializeTags() { person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + logger.fine("Tags initialized for " + person.getName().fullName); + } + + /** + * Sets up the interview details section if start time and duration are present. + * @throws DateTimeParseException if the start time is in an invalid format + */ + private void setupInterviewDetails() { + boolean hasInterview = !person.getStartTime().value.isEmpty() + && !person.getDuration().value.isEmpty(); + + interviewDetailsBox.setVisible(hasInterview); + interviewDetailsBox.setManaged(hasInterview); + + if (hasInterview) { + try { + LocalDateTime startDateTime = LocalDateTime.parse( + person.getStartTime().value, + INPUT_FORMATTER + ); + startTime.setText(startDateTime.format(DISPLAY_FORMATTER)); + duration.setText(person.getDuration().value + MINUTES_SUFFIX); + logger.fine("Interview details set for " + person.getName().fullName); + } catch (DateTimeParseException e) { + logger.warning("Invalid date format for " + person.getName().fullName + ": " + + person.getStartTime().value); + startTime.setText("Invalid date format"); + duration.setText("N/A"); + } + } } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..ccb45d8e062 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -65,7 +65,7 @@ void showAlertDialogAndWait(Alert.AlertType type, String title, String headerTex private static void showAlertDialogAndWait(Stage owner, AlertType type, String title, String headerText, String contentText) { final Alert alert = new Alert(type); - alert.getDialogPane().getStylesheets().add("view/DarkTheme.css"); + alert.getDialogPane().getStylesheets().add("view/LightTheme.css"); alert.initOwner(owner); alert.setTitle(title); alert.setHeaderText(headerText); diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css deleted file mode 100644 index 36e6b001cd8..00000000000 --- a/src/main/resources/view/DarkTheme.css +++ /dev/null @@ -1,352 +0,0 @@ -.background { - -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ -} - -.label { - -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: #555555; - -fx-opacity: 0.9; -} - -.label-bright { - -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: white; - -fx-opacity: 1; -} - -.label-header { - -fx-font-size: 32pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-opacity: 1; -} - -.text-field { - -fx-font-size: 12pt; - -fx-font-family: "Segoe UI Semibold"; -} - -.tab-pane { - -fx-padding: 0 0 0 1; -} - -.tab-pane .tab-header-area { - -fx-padding: 0 0 0 0; - -fx-min-height: 0; - -fx-max-height: 0; -} - -.table-view { - -fx-base: #1d1d1d; - -fx-control-inner-background: #1d1d1d; - -fx-background-color: #1d1d1d; - -fx-table-cell-border-color: transparent; - -fx-table-header-border-color: transparent; - -fx-padding: 5; -} - -.table-view .column-header-background { - -fx-background-color: transparent; -} - -.table-view .column-header, .table-view .filler { - -fx-size: 35; - -fx-border-width: 0 0 1 0; - -fx-background-color: transparent; - -fx-border-color: - transparent - transparent - derive(-fx-base, 80%) - transparent; - -fx-border-insets: 0 10 1 0; -} - -.table-view .column-header .label { - -fx-font-size: 20pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-alignment: center-left; - -fx-opacity: 1; -} - -.table-view:focused .table-row-cell:filled:focused:selected { - -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 { - -fx-border-radius: 1; - -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); -} - -.list-view { - -fx-background-insets: 0; - -fx-padding: 0; - -fx-background-color: derive(#1d1d1d, 20%); -} - -.list-cell { - -fx-label-padding: 0 0 0 0; - -fx-graphic-text-gap : 0; - -fx-padding: 0 0 0 0; -} - -.list-cell:filled:even { - -fx-background-color: #3c3e3f; -} - -.list-cell:filled:odd { - -fx-background-color: #515658; -} - -.list-cell:filled:selected { - -fx-background-color: #424d5f; -} - -.list-cell:filled:selected #cardPane { - -fx-border-color: #3e7b91; - -fx-border-width: 1; -} - -.list-cell .label { - -fx-text-fill: white; -} - -.cell_big_label { - -fx-font-family: "Segoe UI Semibold"; - -fx-font-size: 16px; - -fx-text-fill: #010504; -} - -.cell_small_label { - -fx-font-family: "Segoe UI"; - -fx-font-size: 13px; - -fx-text-fill: #010504; -} - -.stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); - -fx-border-top-width: 1px; -} - -.status-bar { - -fx-background-color: derive(#1d1d1d, 30%); -} - -.result-display { - -fx-background-color: transparent; - -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; - -fx-text-fill: white; -} - -.result-display .label { - -fx-text-fill: black !important; -} - -.status-bar .label { - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-padding: 4px; - -fx-pref-height: 30px; -} - -.status-bar-with-border { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 25%); - -fx-border-width: 1px; -} - -.status-bar-with-border .label { - -fx-text-fill: white; -} - -.grid-pane { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 30%); - -fx-border-width: 1px; -} - -.grid-pane .stack-pane { - -fx-background-color: derive(#1d1d1d, 30%); -} - -.context-menu { - -fx-background-color: derive(#1d1d1d, 50%); -} - -.context-menu .label { - -fx-text-fill: white; -} - -.menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.menu-bar .label { - -fx-font-size: 14pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-opacity: 0.9; -} - -.menu .left-container { - -fx-background-color: black; -} - -/* - * Metro style Push Button - * Author: Pedro Duque Vieira - * http://pixelduke.wordpress.com/2012/10/23/jmetro-windows-8-controls-on-java/ - */ -.button { - -fx-padding: 5 22 5 22; - -fx-border-color: #e2e2e2; - -fx-border-width: 2; - -fx-background-radius: 0; - -fx-background-color: #1d1d1d; - -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; - -fx-font-size: 11pt; - -fx-text-fill: #d8d8d8; - -fx-background-insets: 0 0 0 0, 0, 1, 2; -} - -.button:hover { - -fx-background-color: #3a3a3a; -} - -.button:pressed, .button:default:hover:pressed { - -fx-background-color: white; - -fx-text-fill: #1d1d1d; -} - -.button:focused { - -fx-border-color: white, white; - -fx-border-width: 1, 1; - -fx-border-style: solid, segments(1, 1); - -fx-border-radius: 0, 0; - -fx-border-insets: 1 1 1 1, 0; -} - -.button:disabled, .button:default:disabled { - -fx-opacity: 0.4; - -fx-background-color: #1d1d1d; - -fx-text-fill: white; -} - -.button:default { - -fx-background-color: -fx-focus-color; - -fx-text-fill: #ffffff; -} - -.button:default:hover { - -fx-background-color: derive(-fx-focus-color, 30%); -} - -.dialog-pane { - -fx-background-color: #1d1d1d; -} - -.dialog-pane > *.button-bar > *.container { - -fx-background-color: #1d1d1d; -} - -.dialog-pane > *.label.content { - -fx-font-size: 14px; - -fx-font-weight: bold; - -fx-text-fill: white; -} - -.dialog-pane:header *.header-panel { - -fx-background-color: derive(#1d1d1d, 25%); -} - -.dialog-pane:header *.header-panel *.label { - -fx-font-size: 18px; - -fx-font-style: italic; - -fx-fill: white; - -fx-text-fill: white; -} - -.scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); - -fx-background-insets: 3; -} - -.scroll-bar .increment-button, .scroll-bar .decrement-button { - -fx-background-color: transparent; - -fx-padding: 0 0 0 0; -} - -.scroll-bar .increment-arrow, .scroll-bar .decrement-arrow { - -fx-shape: " "; -} - -.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow { - -fx-padding: 1 8 1 8; -} - -.scroll-bar:horizontal .increment-arrow, .scroll-bar:horizontal .decrement-arrow { - -fx-padding: 8 1 8 1; -} - -#cardPane { - -fx-background-color: transparent; - -fx-border-width: 0; -} - -#commandTypeLabel { - -fx-font-size: 11px; - -fx-text-fill: #F70D1A; -} - -#commandTextField { - -fx-background-color: transparent #383838 transparent #383838; - -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; - -fx-border-insets: 0; - -fx-border-width: 1; - -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; - -fx-text-fill: white; -} - -#filterField, #personListPanel, #personWebpage { - -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); -} - -#resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; - -fx-background-radius: 0; -} - -#tags { - -fx-hgap: 7; - -fx-vgap: 3; -} - -#tags .label { - -fx-text-fill: white; - -fx-background-color: #3e7b91; - -fx-padding: 1 3 1 3; - -fx-border-radius: 2; - -fx-background-radius: 2; - -fx-font-size: 11; -} diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index bfe82a85964..e78990fd8e1 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -1,20 +1,42 @@ - .error { - -fx-text-fill: #d06651 !important; /* The error class should always override the default text-fill style */ + -fx-text-fill: #ff3b30 !important; } .list-cell:empty { - /* Empty cells will not have alternating colours */ - -fx-background: #383838; + -fx-background: #ffffff; } .tag-selector { -fx-border-width: 1; - -fx-border-color: white; - -fx-border-radius: 3; - -fx-background-radius: 3; + -fx-border-color: #e6e6e6; + -fx-border-radius: 6; + -fx-background-radius: 6; } .tooltip-text { - -fx-text-fill: white; + -fx-text-fill: #333333; +} + +/* Apple-style candidate card */ +.candidate-card { + -fx-background-color: white; + -fx-background-radius: 10px; + -fx-border-radius: 10px; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.1), 5, 0, 0, 1); + -fx-padding: 12px; +} + +/* Apple-style section headers */ +.section-header { + -fx-font-family: "SF Pro Display", "Helvetica Neue", Helvetica, sans-serif; + -fx-font-size: 14pt; + -fx-font-weight: bold; + -fx-text-fill: #333333; +} + +/* Apple-style command prompt */ +.command-prompt { + -fx-font-family: "SF Mono", "Menlo", monospace; + -fx-font-size: 12pt; + -fx-text-fill: #0071e3; } diff --git a/src/main/resources/view/LightTheme.css b/src/main/resources/view/LightTheme.css new file mode 100644 index 00000000000..800ad9b044c --- /dev/null +++ b/src/main/resources/view/LightTheme.css @@ -0,0 +1,490 @@ +/* Base styles */ +.background { + -fx-background-color: #ffffff; + background-color: #ffffff; +} + +/* Typography and text styles */ +.label { + -fx-font-size: 11pt; + -fx-font-family: "SF Pro Text", "Helvetica Neue", Helvetica, sans-serif; + -fx-text-fill: #333333; + -fx-opacity: 0.9; +} + +.label-bright { + -fx-font-size: 11pt; + -fx-font-family: "SF Pro Text", "Helvetica Neue", Helvetica, sans-serif; + -fx-text-fill: #000000; + -fx-opacity: 1; +} + +.label-header { + -fx-font-size: 24pt; + -fx-font-family: "SF Pro Display", "Helvetica Neue", Helvetica, sans-serif; + -fx-text-fill: #000000; + -fx-opacity: 1; +} + +/* Form elements */ +.text-field { + -fx-font-size: 12pt; + -fx-font-family: "SF Pro Text", "Helvetica Neue", Helvetica, sans-serif; + -fx-background-color: #f5f5f7; + -fx-border-color: transparent; + -fx-border-radius: 6px; + -fx-background-radius: 6px; +} + +/* Tab styling */ +.tab-pane { + -fx-padding: 0 0 0 1; +} + +.tab-pane .tab-header-area { + -fx-padding: 0 0 0 0; + -fx-min-height: 0; + -fx-max-height: 0; +} + +/* Table view styling */ +.table-view { + -fx-base: #ffffff; + -fx-control-inner-background: #ffffff; + -fx-background-color: #ffffff; + -fx-table-cell-border-color: #e6e6e6; + -fx-table-header-border-color: #e6e6e6; + -fx-padding: 5; +} + +.table-view .column-header-background { + -fx-background-color: #f5f5f7; +} + +.table-view .column-header, .table-view .filler { + -fx-size: 35; + -fx-border-width: 0 0 1 0; + -fx-background-color: transparent; + -fx-border-color: + transparent + transparent + #e6e6e6 + transparent; + -fx-border-insets: 0 10 1 0; +} + +.table-view .column-header .label { + -fx-font-size: 13pt; + -fx-font-family: "SF Pro Text", "Helvetica Neue", Helvetica, sans-serif; + -fx-text-fill: #000000; + -fx-alignment: center-left; + -fx-opacity: 1; +} + +.table-view:focused .table-row-cell:filled:focused:selected { + -fx-background-color: #0071e3; +} + +/* Split pane styling */ +.split-pane:horizontal .split-pane-divider { + -fx-background-color: #f5f5f7; + -fx-border-color: transparent transparent transparent #e6e6e6; +} + +.split-pane { + -fx-border-radius: 0; + -fx-border-width: 0; + -fx-background-color: #ffffff; +} + +/* List view styling */ +.list-view { + -fx-background-insets: 0; + -fx-padding: 0; + -fx-background-color: #ffffff; + -fx-border-radius: 8px; + -fx-min-width: 820px; +} + +.list-cell { + -fx-label-padding: 0 0 0 0; + -fx-graphic-text-gap: 0; + -fx-padding: 12 12 12 12; + -fx-background-radius: 8px; + -fx-border-radius: 8px; +} + +.list-cell:filled:even { + -fx-background-color: #ffffff; +} + +.list-cell:filled:odd { + -fx-background-color: #f5f5f7; +} + +.list-cell:filled:selected { + -fx-background-color: #e8f0fe; +} + +.list-cell:filled:selected #cardPane { + -fx-border-color: #0071e3; + -fx-border-width: 1; + -fx-border-radius: 8px; +} + +.list-cell .label { + -fx-text-fill: #333333; +} + +/* Cell styling */ +.cell_big_label { + -fx-font-family: "SF Pro Display", "Helvetica Neue", Helvetica, sans-serif; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-text-fill: #000000; +} + +.cell_bold_label { + -fx-font-family: "SF Pro Text", "Helvetica Neue", Helvetica, sans-serif; + -fx-font-size: 13px; + -fx-text-fill: #333333; + -fx-font-weight: bold; + -fx-padding: 2 0 2 0; +} + +.cell_small_label { + -fx-font-family: "SF Pro Text", "Helvetica Neue", Helvetica, sans-serif; + -fx-font-size: 13px; + -fx-text-fill: #666666; + -fx-padding: 2 0 2 0; +} + +/* Layout containers */ +.candidate-card { + -fx-background-color: white; + -fx-background-radius: 10px; + -fx-border-radius: 10px; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.1), 5, 0, 0, 1); + -fx-padding: 12px 12px 12px 12px; + -fx-min-width: 800px; + -fx-spacing: 0; +} + +.stack-pane { + -fx-background-color: #ffffff; +} + +.pane-with-border { + -fx-background-color: #ffffff; + -fx-border-color: #e6e6e6; + -fx-border-top-width: 1px; +} + +/* Status bar styling */ +.status-bar { + -fx-background-color: #f5f5f7; +} + +.result-display { + -fx-background-color: #ffffff; + -fx-font-family: "SF Mono", "Menlo", monospace; + -fx-font-size: 13pt; + -fx-text-fill: #333333; + -fx-border-radius: 6px; + -fx-background-radius: 6px; +} + +.result-display .content { + -fx-background-color: #f5f5f7; + -fx-background-radius: 6px; +} + +.result-display .label { + -fx-text-fill: #333333 !important; +} + +.status-bar .label { + -fx-font-family: "SF Pro Text", "Helvetica Neue", Helvetica, sans-serif; + -fx-text-fill: #666666; + -fx-padding: 4px; + -fx-pref-height: 30px; +} + +.status-bar-with-border { + -fx-background-color: #f5f5f7; + -fx-border-color: #e6e6e6; + -fx-border-width: 1px; +} + +.status-bar-with-border .label { + -fx-text-fill: #666666; +} + +/* Grid styling */ +.grid-pane { + -fx-background-color: #ffffff; + -fx-border-color: #e6e6e6; + -fx-border-width: 1px; +} + +.grid-pane .stack-pane { + -fx-background-color: #ffffff; +} + +/* Menu styling */ +.context-menu { + -fx-background-color: #ffffff; + -fx-border-color: #e6e6e6; + -fx-border-radius: 6px; + -fx-background-radius: 6px; +} + +.context-menu .label { + -fx-text-fill: #333333; +} + +.menu-bar { + -fx-background-color: #f5f5f7; + -fx-border-color: #e6e6e6; + -fx-border-width: 0 0 1 0; +} + +.menu-bar .label { + -fx-font-size: 12pt; + -fx-font-family: "SF Pro Text", "Helvetica Neue", Helvetica, sans-serif; + -fx-text-fill: #333333; + -fx-opacity: 0.9; +} + +.menu .left-container { + -fx-background-color: #f5f5f7; +} + +/* Button styling */ +.button { + -fx-padding: 5 15 5 15; + -fx-border-color: transparent; + -fx-border-width: 0; + -fx-background-radius: 6px; + -fx-background-color: #0071e3; + -fx-font-family: "SF Pro Text", "Helvetica Neue", Helvetica, sans-serif; + -fx-font-size: 11pt; + -fx-text-fill: white; + -fx-background-insets: 0; +} + +.button:hover { + -fx-background-color: #0077ed; +} + +.button:pressed, .button:default:hover:pressed { + -fx-background-color: #005bbf; + -fx-text-fill: white; +} + +.button:focused { + -fx-border-color: #0071e3, #0071e3; + -fx-border-width: 1, 1; + -fx-border-style: solid, segments(1, 1); + -fx-border-radius: 6px, 6px; + -fx-border-insets: 1 1 1 1, 0; +} + +.button:disabled, .button:default:disabled { + -fx-opacity: 0.4; + -fx-background-color: #cccccc; + -fx-text-fill: #666666; +} + +.button:default { + -fx-background-color: #0071e3; + -fx-text-fill: #ffffff; +} + +.button:default:hover { + -fx-background-color: #0077ed; +} + +/* Dialog styling */ +.dialog-pane { + -fx-background-color: #ffffff; +} + +.dialog-pane > *.button-bar > *.container { + -fx-background-color: #ffffff; +} + +.dialog-pane > *.label.content { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: #333333; +} + +.dialog-pane:header *.header-panel { + -fx-background-color: #f5f5f7; +} + +.dialog-pane:header *.header-panel *.label { + -fx-font-size: 18px; + -fx-font-style: normal; + -fx-fill: #333333; + -fx-text-fill: #333333; +} + +/* Scrollbar styling */ +.scroll-bar { + -fx-background-color: #f5f5f7; + -fx-background-radius: 10px; +} + +.scroll-bar .thumb { + -fx-background-color: #c1c1c1; + -fx-background-insets: 3; + -fx-background-radius: 10px; +} + +.scroll-bar .increment-button, .scroll-bar .decrement-button { + -fx-background-color: transparent; + -fx-padding: 0 0 0 0; +} + +.scroll-bar .increment-arrow, .scroll-bar .decrement-arrow { + -fx-shape: " "; +} + +.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow { + -fx-padding: 1 8 1 8; +} + +.scroll-bar:horizontal .increment-arrow, .scroll-bar:horizontal .decrement-arrow { + -fx-padding: 8 1 8 1; +} + +/* Card styling */ +#cardPane { + -fx-background-color: #ffffff; + -fx-border-width: 0; + -fx-border-radius: 8px; + -fx-background-radius: 8px; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.1), 5, 0, 0, 1); + -fx-padding: 5 0 5 0; +} + +/* Command interface styling */ +#commandTypeLabel { + -fx-font-size: 11px; + -fx-text-fill: #0071e3; +} + +#commandTextField { + -fx-background-color: #f5f5f7; + -fx-background-insets: 0; + -fx-border-color: transparent; + -fx-border-insets: 0; + -fx-border-width: 0; + -fx-font-family: "SF Mono", "Menlo", monospace; + -fx-font-size: 13pt; + -fx-text-fill: #333333; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-prompt-text-fill: #999999; +} + +#filterField, #personListPanel, #personWebpage { + -fx-effect: null; +} + +#resultDisplay .content { + -fx-background-color: #f5f5f7; + -fx-background-radius: 6px; +} + +/* Tag styling */ +#tags { + -fx-hgap: 7; + -fx-vgap: 3; + -fx-padding: 0 0 8 0; +} + +#tags .label { + -fx-text-fill: white; + -fx-background-color: #0071e3; + -fx-padding: 3 8 3 8; + -fx-border-radius: 12; + -fx-background-radius: 12; + -fx-font-size: 11; +} + +/* Notes section styling */ +.notes-section { + -fx-background-color: #f5f5f7; + -fx-background-radius: 8px; + -fx-border-radius: 8px; + -fx-border-color: #e6e6e6; + -fx-border-width: 1px; + -fx-min-width: 550px; + -fx-pref-width: 550px; + -fx-max-width: 600px; + -fx-padding: 8px 10px 8px 8px; + -fx-translate-x: -40; +} + +.notes-header { + -fx-font-family: "SF Pro Text", "Helvetica Neue", Helvetica, sans-serif; + -fx-font-size: 13px; + -fx-font-weight: bold; + -fx-text-fill: #333333; +} + +.notes-display-container { + -fx-background-color: #ffffff; + -fx-background-radius: 6px; + -fx-border-radius: 6px; + -fx-border-color: #e6e6e6; + -fx-border-width: 1px; +} + +.notes-content { + -fx-font-family: "SF Pro Text", "Helvetica Neue", Helvetica, sans-serif; + -fx-font-size: 13px; + -fx-text-fill: #333333; + -fx-wrap-text: true; + -fx-pref-width: 500px; + -fx-max-width: 500px; + -fx-line-spacing: 4px; + -fx-text-alignment: left; +} + +/* Department tag colors */ +.tag-hardware { + -fx-background-color: #ff9500 !important; +} + +.tag-software { + -fx-background-color: #007aff !important; +} + +.tag-services { + -fx-background-color: #5ac8fa !important; +} + +/* Status indicators */ +.status-new { + -fx-background-color: #34c759 !important; +} + +.status-in-progress { + -fx-background-color: #ff9500 !important; +} + +.status-reviewed { + -fx-background-color: #5856d6 !important; +} + +/* Command prompt styling */ +.command-prompt { + -fx-font-family: "SF Mono", "Menlo", monospace; + -fx-font-size: 12pt; + -fx-text-fill: #0071e3; + -fx-font-weight: bold; +} diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..01840f6288b 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -12,14 +12,14 @@ + title="RecruitIntel" minWidth="850" minHeight="600" onCloseRequest="#handleExit"> - + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index 84e09833a87..edcde94f27c 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -2,35 +2,74 @@ - - + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + diff --git a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json index 6a4d2b7181c..b106b295c3f 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json @@ -3,11 +3,15 @@ "name": "Valid Person", "phone": "9482424", "email": "hans@example.com", - "address": "4th street" + "address": "4th street", + "jobPosition": "Software Engineer", + "team": "Engineering" }, { "name": "Person With Invalid Phone Field", "phone": "948asdf2424", "email": "hans@example.com", - "address": "4th street" + "address": "4th street", + "jobPosition": "Product Manager", + "team": "Product" } ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json index a7427fe7aa2..373a4fd9303 100644 --- a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json @@ -4,11 +4,15 @@ "phone": "94351253", "email": "alice@example.com", "address": "123, Jurong West Ave 6, #08-111", + "jobPosition": "Software Engineer", + "team": "Engineering", "tags": [ "friends" ] }, { "name": "Alice Pauline", "phone": "94351253", - "email": "pauline@example.com", - "address": "4th street" + "email": "alice@example.com", + "address": "4th street", + "jobPosition": "Software Engineer", + "team": "Engineering" } ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json index ad3f135ae42..adb79e060a0 100644 --- a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json @@ -3,6 +3,8 @@ "name": "Hans Muster", "phone": "9482424", "email": "invalid@email!3e", - "address": "4th street" + "address": "4th street", + "jobPosition": "@invalid job!", + "team": "@invalid team!" } ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json index 72262099d35..932f3c93674 100644 --- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json @@ -1,46 +1,60 @@ { - "_comment": "AddressBook save file which contains the same Person values as in TypicalPersons#getTypicalAddressBook()", + "_comment": "RecruitIntel save file which contains the same Person values as in TypicalPersons#getTypicalRecruitIntel()", "persons" : [ { "name" : "Alice Pauline", "phone" : "94351253", "email" : "alice@example.com", "address" : "123, Jurong West Ave 6, #08-111", + "jobPosition" : "Software Engineer", + "team" : "Engineering", "tags" : [ "friends" ] }, { "name" : "Benson Meier", "phone" : "98765432", "email" : "johnd@example.com", "address" : "311, Clementi Ave 2, #02-25", + "jobPosition" : "Product Manager", + "team" : "Product", "tags" : [ "owesMoney", "friends" ] }, { "name" : "Carl Kurz", "phone" : "95352563", "email" : "heinz@example.com", "address" : "wall street", + "jobPosition" : "UI Designer", + "team" : "Design", "tags" : [ ] }, { "name" : "Daniel Meier", "phone" : "87652533", "email" : "cornelia@example.com", "address" : "10th street", + "jobPosition" : "Data Scientist", + "team" : "Data", "tags" : [ "friends" ] }, { "name" : "Elle Meyer", "phone" : "9482224", "email" : "werner@example.com", "address" : "michegan ave", + "jobPosition" : "DevOps Engineer", + "team" : "Infrastructure", "tags" : [ ] }, { "name" : "Fiona Kunz", "phone" : "9482427", "email" : "lydia@example.com", "address" : "little tokyo", + "jobPosition" : "Backend Engineer", + "team" : "Engineering", "tags" : [ ] }, { "name" : "George Best", "phone" : "9482442", "email" : "anna@example.com", "address" : "4th street", + "jobPosition" : "Frontend Engineer", + "team" : "Engineering", "tags" : [ ] } ] } diff --git a/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json b/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json index 1037548a9cd..d0fad499aa0 100644 --- a/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json +++ b/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json @@ -9,5 +9,5 @@ "z" : 99 } }, - "addressBookFilePath" : "addressbook.json" + "addressBookFilePath" : "recruitintel.json" } diff --git a/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json b/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json index b819bed900a..c6a61e57cb7 100644 --- a/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json +++ b/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json @@ -7,5 +7,5 @@ "y" : 100 } }, - "addressBookFilePath" : "addressbook.json" + "addressBookFilePath" : "recruitintel.json" } diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java index baf8ce336a2..37ab45e7558 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/seedu/address/logic/LogicManagerTest.java @@ -1,12 +1,14 @@ package seedu.address.logic; import static org.junit.jupiter.api.Assertions.assertEquals; -import static seedu.address.logic.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.JOB_POSITION_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; +import static seedu.address.logic.commands.CommandTestUtil.TEAM_DESC_AMY; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.AMY; @@ -46,7 +48,7 @@ public class LogicManagerTest { @BeforeEach public void setUp() { JsonAddressBookStorage addressBookStorage = - new JsonAddressBookStorage(temporaryFolder.resolve("addressBook.json")); + new JsonAddressBookStorage(temporaryFolder.resolve("recruitintel.json")); JsonUserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(temporaryFolder.resolve("userPrefs.json")); StorageManager storage = new StorageManager(addressBookStorage, userPrefsStorage); logic = new LogicManager(model, storage); @@ -61,7 +63,7 @@ public void execute_invalidCommandFormat_throwsParseException() { @Test public void execute_commandExecutionError_throwsCommandException() { String deleteCommand = "delete 9"; - assertCommandException(deleteCommand, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + assertCommandException(deleteCommand, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } @Test @@ -95,7 +97,7 @@ public void getFilteredPersonList_modifyList_throwsUnsupportedOperationException * @see #assertCommandFailure(String, Class, String, Model) */ private void assertCommandSuccess(String inputCommand, String expectedMessage, - Model expectedModel) throws CommandException, ParseException { + Model expectedModel) throws CommandException, ParseException { CommandResult result = logic.execute(inputCommand); assertEquals(expectedMessage, result.getFeedbackToUser()); assertEquals(expectedModel, model); @@ -122,7 +124,7 @@ private void assertCommandException(String inputCommand, String expectedMessage) * @see #assertCommandFailure(String, Class, String, Model) */ private void assertCommandFailure(String inputCommand, Class expectedException, - String expectedMessage) { + String expectedMessage) { Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); assertCommandFailure(inputCommand, expectedException, expectedMessage, expectedModel); } @@ -135,7 +137,7 @@ private void assertCommandFailure(String inputCommand, Class expectedException, - String expectedMessage, Model expectedModel) { + String expectedMessage, Model expectedModel) { assertThrows(expectedException, expectedMessage, () -> logic.execute(inputCommand)); assertEquals(expectedModel, model); } @@ -166,8 +168,9 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) // Triggers the saveAddressBook method by executing an add command String addCommand = AddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY - + EMAIL_DESC_AMY + ADDRESS_DESC_AMY; - Person expectedPerson = new PersonBuilder(AMY).withTags().build(); + + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + JOB_POSITION_DESC_AMY + TEAM_DESC_AMY + + TAG_DESC_FRIEND; + Person expectedPerson = new PersonBuilder(AMY).build(); ModelManager expectedModel = new ModelManager(); expectedModel.addPerson(expectedPerson); assertCommandFailure(addCommand, CommandException.class, expectedMessage, expectedModel); diff --git a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java b/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java index 162a0c86031..46f27ddee6c 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandIntegrationTest.java @@ -42,7 +42,7 @@ public void execute_newPerson_success() { public void execute_duplicatePerson_throwsCommandException() { Person personInList = model.getAddressBook().getPersonList().get(0); assertCommandFailure(new AddCommand(personInList), model, - AddCommand.MESSAGE_DUPLICATE_PERSON); + String.format(AddCommand.MESSAGE_DUPLICATE_PERSON, personInList.getEmail())); } } diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java index 90e8253f48e..0c1e41f4a16 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -10,6 +10,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.function.Predicate; import org.junit.jupiter.api.Test; @@ -50,7 +51,8 @@ public void execute_duplicatePerson_throwsCommandException() { AddCommand addCommand = new AddCommand(validPerson); ModelStub modelStub = new ModelStubWithPerson(validPerson); - assertThrows(CommandException.class, AddCommand.MESSAGE_DUPLICATE_PERSON, () -> addCommand.execute(modelStub)); + String expectedMessage = String.format(AddCommand.MESSAGE_DUPLICATE_PERSON, validPerson.getEmail()); + assertThrows(CommandException.class, expectedMessage, () -> addCommand.execute(modelStub)); } @Test @@ -157,6 +159,26 @@ public ObservableList getFilteredPersonList() { public void updateFilteredPersonList(Predicate predicate) { throw new AssertionError("This method should not be called."); } + + @Override + public void sortFilteredPersonList(Comparator comparator) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void commit() { + + } + + @Override + public void undo() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void redo() { + throw new AssertionError("This method should not be called."); + } } /** diff --git a/src/test/java/seedu/address/logic/commands/ClassifyCommandTest.java b/src/test/java/seedu/address/logic/commands/ClassifyCommandTest.java new file mode 100644 index 00000000000..883ba5154b5 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ClassifyCommandTest.java @@ -0,0 +1,132 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.DANIEL; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.JobPositionContainsKeywordsPredicate; +import seedu.address.model.person.Person; +import seedu.address.model.person.TagsContainsKeywordsPredicate; +import seedu.address.model.person.TeamContainsKeywordsPredicate; + +/** + * Contains integration tests (interaction with the Model) for {@code ClassifyCommand}. + */ +public class ClassifyCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void equals() { + TagsContainsKeywordsPredicate firstPredicate = + new TagsContainsKeywordsPredicate(Collections.singletonList("first")); + TagsContainsKeywordsPredicate secondPredicate = + new TagsContainsKeywordsPredicate(Collections.singletonList("second")); + + List> firstPredicates = new ArrayList<>(); + firstPredicates.add(firstPredicate); + List> secondPredicates = new ArrayList<>(); + secondPredicates.add(secondPredicate); + + ClassifyCommand classifyFirstCommand = new ClassifyCommand(firstPredicates); + ClassifyCommand classifySecondCommand = new ClassifyCommand(secondPredicates); + + // same object -> returns true + assertTrue(classifyFirstCommand.equals(classifyFirstCommand)); + + // same values -> returns true + ClassifyCommand classifyFirstCommandCopy = new ClassifyCommand(firstPredicates); + assertTrue(classifyFirstCommand.equals(classifyFirstCommandCopy)); + + // different types -> returns false + assertFalse(classifyFirstCommand.equals(1)); + + // null -> returns false + assertFalse(classifyFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(classifyFirstCommand.equals(classifySecondCommand)); + } + + @Test + public void execute_zeroKeywords_noPersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + TagsContainsKeywordsPredicate predicate = preparePredicate(" "); + List> predicates = new ArrayList<>(); + predicates.add(predicate); + ClassifyCommand command = new ClassifyCommand(predicates); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredPersonList()); + } + + @Test + public void classifyCommand_singleTagFriend_threePersonsFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); + List> predicates = new ArrayList<>(); + predicates.add(new TagsContainsKeywordsPredicate(List.of("friend"))); + ClassifyCommand command = new ClassifyCommand(predicates); + expectedModel.updateFilteredPersonList(person -> predicates.stream().allMatch(pred -> pred.test(person))); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE, BENSON, DANIEL), model.getFilteredPersonList()); + } + + @Test + public void classifyCommand_twoTagsWithAndLogic_onePersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 1); + List> predicates = new ArrayList<>(); + predicates.add(new TagsContainsKeywordsPredicate(List.of("friends"))); + predicates.add(new TagsContainsKeywordsPredicate(List.of("owesMoney"))); + ClassifyCommand command = new ClassifyCommand(predicates); + expectedModel.updateFilteredPersonList(person -> predicates.stream().allMatch(pred -> pred.test(person))); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(BENSON), model.getFilteredPersonList()); + } + + @Test + public void classifyCommand_tagsWithJobAndTeam_onePersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 1); + List> predicates = new ArrayList<>(); + predicates.add(new TagsContainsKeywordsPredicate(Arrays.asList("friends", "owesMoney"))); + predicates.add(new JobPositionContainsKeywordsPredicate(Arrays.asList("Product Manager"))); + predicates.add(new TeamContainsKeywordsPredicate(Arrays.asList("Product"))); + ClassifyCommand command = new ClassifyCommand(predicates); + expectedModel.updateFilteredPersonList(person -> predicates.stream().allMatch(pred -> pred.test(person))); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(BENSON), model.getFilteredPersonList()); + } + + @Test + public void toStringMethod() { + TagsContainsKeywordsPredicate predicate = new TagsContainsKeywordsPredicate(Arrays.asList("keyword")); + List> predicates = new ArrayList<>(); + predicates.add(predicate); + ClassifyCommand classifyCommand = new ClassifyCommand(predicates); + String expected = ClassifyCommand.class.getCanonicalName() + "{predicates=" + predicates + "}"; + assertEquals(expected, classifyCommand.toString()); + } + + /** + * Parses {@code userInput} into a {@code TagsContainsKeywordsPredicate}. + */ + private TagsContainsKeywordsPredicate preparePredicate(String userInput) { + return new TagsContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java index 643a1d08069..78c12d41924 100644 --- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java @@ -4,9 +4,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue; 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_JOB_POSITION; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TEAM; import static seedu.address.testutil.Assert.assertThrows; import java.util.ArrayList; @@ -32,8 +34,13 @@ public class CommandTestUtil { public static final String VALID_PHONE_BOB = "22222222"; public static final String VALID_EMAIL_AMY = "amy@example.com"; public static final String VALID_EMAIL_BOB = "bob@example.com"; + public static final String VALID_EMAIL_ALICE = "alice@example.com"; public static final String VALID_ADDRESS_AMY = "Block 312, Amy Street 1"; public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3"; + public static final String VALID_JOB_POSITION_AMY = "Software Engineer"; + public static final String VALID_JOB_POSITION_BOB = "Product Manager"; + public static final String VALID_TEAM_AMY = "Engineering"; + public static final String VALID_TEAM_BOB = "Product"; public static final String VALID_TAG_HUSBAND = "husband"; public static final String VALID_TAG_FRIEND = "friend"; @@ -45,14 +52,28 @@ public class CommandTestUtil { public static final String EMAIL_DESC_BOB = " " + PREFIX_EMAIL + VALID_EMAIL_BOB; public static final String ADDRESS_DESC_AMY = " " + PREFIX_ADDRESS + VALID_ADDRESS_AMY; public static final String ADDRESS_DESC_BOB = " " + PREFIX_ADDRESS + VALID_ADDRESS_BOB; + public static final String JOB_POSITION_DESC_AMY = " " + PREFIX_JOB_POSITION + VALID_JOB_POSITION_AMY; + public static final String JOB_POSITION_DESC_BOB = " " + PREFIX_JOB_POSITION + VALID_JOB_POSITION_BOB; + public static final String TEAM_DESC_AMY = " " + PREFIX_TEAM + VALID_TEAM_AMY; + public static final String TEAM_DESC_BOB = " " + PREFIX_TEAM + VALID_TEAM_BOB; public static final String TAG_DESC_FRIEND = " " + PREFIX_TAG + VALID_TAG_FRIEND; public static final String TAG_DESC_HUSBAND = " " + PREFIX_TAG + VALID_TAG_HUSBAND; - public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names - public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones - public static final String INVALID_EMAIL_DESC = " " + PREFIX_EMAIL + "bob!yahoo"; // missing '@' symbol - public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS; // empty string not allowed for addresses - public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags + public static final String INVALID_NAME = "R@chel"; + public static final String INVALID_PHONE = "+651234"; + public static final String INVALID_EMAIL = "example.com"; + public static final String INVALID_ADDRESS = " "; + public static final String INVALID_JOB_POSITION = "@invalid job!"; + public static final String INVALID_TEAM = "@invalid team!"; + public static final String INVALID_TAG = "@!invalid"; + + public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + INVALID_NAME; + public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + INVALID_PHONE; + public static final String INVALID_EMAIL_DESC = " " + PREFIX_EMAIL + INVALID_EMAIL; + public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS + INVALID_ADDRESS; + public static final String INVALID_JOB_POSITION_DESC = " " + PREFIX_JOB_POSITION + INVALID_JOB_POSITION; + public static final String INVALID_TEAM_DESC = " " + PREFIX_TEAM + INVALID_TEAM; + public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + INVALID_TAG; public static final String PREAMBLE_WHITESPACE = "\t \r \n"; public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; @@ -63,9 +84,11 @@ public class CommandTestUtil { static { DESC_AMY = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) + .withJobPosition(VALID_JOB_POSITION_AMY).withTeam(VALID_TEAM_AMY) .withTags(VALID_TAG_FRIEND).build(); DESC_BOB = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) + .withJobPosition(VALID_JOB_POSITION_BOB).withTeam(VALID_TEAM_BOB) .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); } @@ -75,7 +98,7 @@ public class CommandTestUtil { * - the {@code actualModel} matches {@code expectedModel} */ public static void assertCommandSuccess(Command command, Model actualModel, CommandResult expectedCommandResult, - Model expectedModel) { + Model expectedModel) { try { CommandResult result = command.execute(actualModel); assertEquals(expectedCommandResult, result); @@ -90,7 +113,7 @@ public static void assertCommandSuccess(Command command, Model actualModel, Comm * that takes a string {@code expectedMessage}. */ public static void assertCommandSuccess(Command command, Model actualModel, String expectedMessage, - Model expectedModel) { + Model expectedModel) { CommandResult expectedCommandResult = new CommandResult(expectedMessage); assertCommandSuccess(command, actualModel, expectedCommandResult, expectedModel); } @@ -99,7 +122,7 @@ public static void assertCommandSuccess(Command command, Model actualModel, Stri * Executes the given {@code command}, confirms that
* - a {@code CommandException} is thrown
* - the CommandException message matches {@code expectedMessage}
- * - the address book, filtered person list and selected person in {@code actualModel} remain unchanged + * - the RecruitIntel, filtered person list and selected person in {@code actualModel} remain unchanged */ public static void assertCommandFailure(Command command, Model actualModel, String expectedMessage) { // we are unable to defensively copy the model for comparison later, so we can @@ -113,7 +136,7 @@ public static void assertCommandFailure(Command command, Model actualModel, Stri } /** * Updates {@code model}'s filtered list to show only the person at the given {@code targetIndex} in the - * {@code model}'s address book. + * {@code model}'s RecruitIntel. */ public static void showPersonAtIndex(Model model, Index targetIndex) { assertTrue(targetIndex.getZeroBased() < model.getFilteredPersonList().size()); diff --git a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java index b6f332eabca..a91d0bcd161 100644 --- a/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/DeleteCommandTest.java @@ -71,7 +71,7 @@ public void execute_invalidIndexFilteredList_throwsCommandException() { showPersonAtIndex(model, INDEX_FIRST_PERSON); Index outOfBoundIndex = INDEX_SECOND_PERSON; - // ensures that outOfBoundIndex is still in bounds of address book list + // ensures that outOfBoundIndex is still in bounds of RecruitIntel list assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); DeleteCommand deleteCommand = new DeleteCommand(outOfBoundIndex); diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java index 469dd97daa7..749a64420c8 100644 --- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java @@ -112,7 +112,7 @@ public void execute_duplicatePersonUnfilteredList_failure() { public void execute_duplicatePersonFilteredList_failure() { showPersonAtIndex(model, INDEX_FIRST_PERSON); - // edit person in filtered list into a duplicate in address book + // edit person in filtered list into a duplicate in RecruitIntel Person personInList = model.getAddressBook().getPersonList().get(INDEX_SECOND_PERSON.getZeroBased()); EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, new EditPersonDescriptorBuilder(personInList).build()); @@ -131,13 +131,13 @@ public void execute_invalidPersonIndexUnfilteredList_failure() { /** * Edit filtered list where index is larger than size of filtered list, - * but smaller than size of address book + * but smaller than size of RecruitIntel */ @Test public void execute_invalidPersonIndexFilteredList_failure() { showPersonAtIndex(model, INDEX_FIRST_PERSON); Index outOfBoundIndex = INDEX_SECOND_PERSON; - // ensures that outOfBoundIndex is still in bounds of address book list + // ensures that outOfBoundIndex is still in bounds of RecruitIntel list assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); EditCommand editCommand = new EditCommand(outOfBoundIndex, diff --git a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java index b17c1f3d5c2..27e8110caf3 100644 --- a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java +++ b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java @@ -60,12 +60,10 @@ public void equals() { @Test public void toStringMethod() { EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); - String expected = EditPersonDescriptor.class.getCanonicalName() + "{name=" - + editPersonDescriptor.getName().orElse(null) + ", phone=" - + editPersonDescriptor.getPhone().orElse(null) + ", email=" - + editPersonDescriptor.getEmail().orElse(null) + ", address=" - + editPersonDescriptor.getAddress().orElse(null) + ", tags=" - + editPersonDescriptor.getTags().orElse(null) + "}"; + String expected = EditPersonDescriptor.class.getCanonicalName() + + "{name=null, phone=null, email=null, address=null, " + + "jobPosition=null, team=null, tags=null, notes=null, " + + "interview time=null, duration=null}"; assertEquals(expected, editPersonDescriptor.toString()); } } diff --git a/src/test/java/seedu/address/logic/commands/InterviewCommandTest.java b/src/test/java/seedu/address/logic/commands/InterviewCommandTest.java new file mode 100644 index 00000000000..a1ad60a95d7 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/InterviewCommandTest.java @@ -0,0 +1,104 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Duration; +import seedu.address.model.person.Person; +import seedu.address.model.person.StartTime; + +public class InterviewCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_setInterview_success() { + Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + + StartTime startTime = new StartTime("2025-04-01 10:00"); + Duration duration = new Duration("30"); + + Person editedPerson = new Person(firstPerson.getName(), firstPerson.getPhone(), firstPerson.getEmail(), + firstPerson.getAddress(), firstPerson.getJobPosition(), firstPerson.getTeam(), + firstPerson.getTags(), firstPerson.getNotes(), startTime, duration); + + InterviewCommand command = new InterviewCommand(INDEX_FIRST_PERSON, startTime, duration); + + String expectedMessage = String.format(InterviewCommand.MESSAGE_SET_INTERVIEW_SUCCESS, + INDEX_FIRST_PERSON.getOneBased(), startTime.value, duration.getDurationInMinutes()); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(firstPerson, editedPerson); + + assertCommandSuccess(command, model, expectedMessage, expectedModel); + } + + @Test + public void execute_invalidPersonIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + InterviewCommand interviewCommand = new InterviewCommand(outOfBoundIndex, + new StartTime("2025-04-01 14:00"), new Duration("30")); + + assertCommandFailure(interviewCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_invalidPersonIndexFilteredList_failure() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + Index outOfBoundIndex = INDEX_SECOND_PERSON; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + InterviewCommand interviewCommand = new InterviewCommand(outOfBoundIndex, + new StartTime("2025-04-01 14:00"), new Duration("30")); + + assertCommandFailure(interviewCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void equals() { + StartTime startTime = new StartTime("2025-04-01 10:00"); + Duration duration = new Duration("30"); + + final InterviewCommand standardCommand = new InterviewCommand(INDEX_FIRST_PERSON, startTime, duration); + + // same values -> returns true + InterviewCommand sameCommand = new InterviewCommand(INDEX_FIRST_PERSON, + new StartTime("2025-04-01 10:00"), new Duration("30")); + assertTrue(standardCommand.equals(sameCommand)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different type -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new InterviewCommand(INDEX_SECOND_PERSON, startTime, duration))); + + // different start time -> returns false + assertFalse(standardCommand.equals(new InterviewCommand(INDEX_FIRST_PERSON, + new StartTime("2025-04-01 11:00"), duration))); + + // different duration -> returns false + assertFalse(standardCommand.equals(new InterviewCommand(INDEX_FIRST_PERSON, + startTime, new Duration("45")))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/NotesCommandTest.java b/src/test/java/seedu/address/logic/commands/NotesCommandTest.java new file mode 100644 index 00000000000..cefa03cf955 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/NotesCommandTest.java @@ -0,0 +1,92 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Notes; +import seedu.address.model.person.Person; + + +public class NotesCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void executeNotesCommand_validIndex_addsNotesSuccessfully() { + Person firstPerson = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); + Person editedPerson = new Person(firstPerson.getName(), firstPerson.getPhone(), firstPerson.getEmail(), + firstPerson.getAddress(), firstPerson.getJobPosition(), firstPerson.getTeam(), + firstPerson.getTags(), new Notes("Test notes"), firstPerson.getStartTime(), + firstPerson.getDuration()); + + NotesCommand notesCommand = new NotesCommand(INDEX_FIRST_PERSON, new Notes("Test notes")); + + String expectedMessage = String.format(NotesCommand.MESSAGE_ADD_NOTES_SUCCESS, + INDEX_FIRST_PERSON.getOneBased(), "Test notes"); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(firstPerson, editedPerson); + + assertCommandSuccess(notesCommand, model, expectedMessage, expectedModel); + } + + @Test + public void executeNotesCommand_invalidIndexUnfiltered_throwsCommandException() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + NotesCommand notesCommand = new NotesCommand(outOfBoundIndex, new Notes("Test notes")); + + assertCommandFailure(notesCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void executeNotesCommand_invalidIndexFiltered_throwsCommandException() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + Index outOfBoundIndex = INDEX_SECOND_PERSON; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + NotesCommand notesCommand = new NotesCommand(outOfBoundIndex, new Notes("Test notes")); + + assertCommandFailure(notesCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void equalsMethod_variousScenarios_returnsExpectedResults() { + final Notes notes = new Notes("Test notes"); + final NotesCommand standardCommand = new NotesCommand(INDEX_FIRST_PERSON, notes); + + // same values -> returns true + NotesCommand commandWithSameValues = new NotesCommand(INDEX_FIRST_PERSON, notes); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new NotesCommand(INDEX_SECOND_PERSON, notes))); + + // different notes -> returns false + Notes differentNotes = new Notes("Different notes"); + assertFalse(standardCommand.equals(new NotesCommand(INDEX_FIRST_PERSON, differentNotes))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/RedoCommandTest.java b/src/test/java/seedu/address/logic/commands/RedoCommandTest.java new file mode 100644 index 00000000000..95c174299ad --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/RedoCommandTest.java @@ -0,0 +1,165 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; + +public class RedoCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_noUndoToRedo_throwsCommandException() { + RedoCommand redoCommand = new RedoCommand(); + assertThrows(CommandException.class, () -> redoCommand.execute(model)); + } + + @Test + public void execute_redoAfterUndo_success() { + Person firstPerson = model.getFilteredPersonList().get(6); + + DeleteCommand deleteCommand = new DeleteCommand(firstPerson); + try { + deleteCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + UndoCommand undoCommand = new UndoCommand(); + try { + undoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + RedoCommand redoCommand = new RedoCommand(); + try { + redoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + assertEquals(model.getFilteredPersonList().size(), 6); + } + + @Test + public void execute_redoMultipleUndos_success() { + Person firstPerson = model.getFilteredPersonList().get(6); + Person secondPerson = model.getFilteredPersonList().get(5); + + DeleteCommand deleteFirstCommand = new DeleteCommand(firstPerson); + DeleteCommand deleteSecondCommand = new DeleteCommand(secondPerson); + + try { + deleteFirstCommand.execute(model); + deleteSecondCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + UndoCommand undoCommand = new UndoCommand(); + try { + undoCommand.execute(model); + undoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + RedoCommand redoCommand = new RedoCommand(); + try { + redoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + assertEquals(model.getFilteredPersonList().size(), 6); + + try { + redoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + assertEquals(model.getFilteredPersonList().size(), 5); + } + + @Test + public void execute_noRedoAfterModifyingCommand_throwsCommandException() { + Person firstPerson = model.getFilteredPersonList().get(6); + Person secondPerson = model.getFilteredPersonList().get(5); + + DeleteCommand deleteFirstCommand = new DeleteCommand(firstPerson); + DeleteCommand deleteSecondCommand = new DeleteCommand(secondPerson); + + try { + deleteFirstCommand.execute(model); + deleteSecondCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + UndoCommand undoCommand = new UndoCommand(); + + try { + undoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + Person thirdPerson = model.getFilteredPersonList().get(4); + DeleteCommand deleteThirdCommand = new DeleteCommand(thirdPerson); + + try { + deleteThirdCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + RedoCommand redoCommand = new RedoCommand(); + assertThrows(CommandException.class, () -> redoCommand.execute(model)); + } + + @Test + public void execute_redoNonModifyingCommand_noChange() { + Person newPerson = model.getFilteredPersonList().get(0); + + DeleteCommand deleteCommand = new DeleteCommand(newPerson); + + try { + deleteCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + UndoCommand undoCommand = new UndoCommand(); + + try { + undoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + ListCommand listCommand = new ListCommand(); + listCommand.execute(model); + + SortCommand sortCommand = new SortCommand(); + sortCommand.execute(model); + + RedoCommand redoCommand = new RedoCommand(); + + try { + redoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + assertEquals(model.getFilteredPersonList().size(), 6); + } +} diff --git a/src/test/java/seedu/address/logic/commands/SortCommandTest.java b/src/test/java/seedu/address/logic/commands/SortCommandTest.java new file mode 100644 index 00000000000..3f1e6ae4b84 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/SortCommandTest.java @@ -0,0 +1,61 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; + +public class SortCommandTest { + + private final Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private final Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_sortByStartTime_success() { + expectedModel.sortFilteredPersonList(Comparator.comparing(p -> p.getStartTime())); + assertCommandSuccess(new SortCommand(), model, SortCommand.MESSAGE_SORT_SUCCESS, expectedModel); + } + + @Test + public void execute_sortOnEmptyList_success() { + Model emptyModel = new ModelManager(new AddressBook(), new UserPrefs()); + Model expectedEmptyModel = new ModelManager(new AddressBook(), new UserPrefs()); + assertCommandSuccess(new SortCommand(), emptyModel, SortCommand.MESSAGE_SORT_SUCCESS, expectedEmptyModel); + } + + @Test + public void equals() { + SortCommand firstSort = new SortCommand(); + SortCommand secondSort = new SortCommand(); + + // same object -> returns true + assertTrue(firstSort.equals(firstSort)); + + // different objects, same logic -> returns true + assertTrue(firstSort.equals(secondSort)); + + // null -> returns false + assertFalse(firstSort.equals(null)); + + // different type -> returns false + assertFalse(firstSort.equals(new ClearCommand())); + } + + @Test + public void toStringMethod() { + SortCommand sortCommand = new SortCommand(); + String expected = SortCommand.class.getCanonicalName() + "{}"; + System.out.println(expected); + System.out.println(sortCommand); + assertTrue(sortCommand.toString().startsWith(expected)); // accept formatting extensions + } +} diff --git a/src/test/java/seedu/address/logic/commands/UndoCommandTest.java b/src/test/java/seedu/address/logic/commands/UndoCommandTest.java new file mode 100644 index 00000000000..0969668b853 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/UndoCommandTest.java @@ -0,0 +1,142 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; + +/** + * Contains integration tests (interaction with the Model) and unit tests for UndoCommand. + */ +public class UndoCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_undoDeleteCommand_success() { + Person firstPerson = model.getFilteredPersonList().get(6); + + DeleteCommand deleteCommand = new DeleteCommand(firstPerson); + try { + deleteCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + UndoCommand undoCommand = new UndoCommand(); + + try { + undoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + assertEquals(model.getFilteredPersonList().size(), 7); + } + + @Test + public void execute_noCommandToUndo_throwsCommandException() { + UndoCommand undoCommand = new UndoCommand(); + assertThrows(CommandException.class, () -> undoCommand.execute(model)); + } + + @Test + public void execute_undoMultipleCommands_success() { + Person firstPerson = model.getFilteredPersonList().get(6); + Person secondPerson = model.getFilteredPersonList().get(5); + + DeleteCommand deleteFirstCommand = new DeleteCommand(firstPerson); + DeleteCommand deleteSecondCommand = new DeleteCommand(secondPerson); + + try { + deleteFirstCommand.execute(model); + deleteSecondCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + UndoCommand undoCommand = new UndoCommand(); + + try { + undoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + assertEquals(model.getFilteredPersonList().size(), 6); + + try { + undoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + assertEquals(model.getFilteredPersonList().size(), 7); + } + + @Test + public void execute_undoAddCommand_success() { + Person newPerson = model.getFilteredPersonList().get(0); + + AddCommand addCommand = new AddCommand(newPerson); + DeleteCommand deleteCommand = new DeleteCommand(newPerson); + + try { + deleteCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + try { + addCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + UndoCommand undoCommand = new UndoCommand(); + + try { + undoCommand.execute(model); + undoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + assertEquals(model.getFilteredPersonList().get(0), newPerson); + } + + @Test + public void execute_undoNonModifyingCommand_noChange() { + Person newPerson = model.getFilteredPersonList().get(0); + + DeleteCommand deleteCommand = new DeleteCommand(newPerson); + + try { + deleteCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + ListCommand listCommand = new ListCommand(); + listCommand.execute(model); + + SortCommand sortCommand = new SortCommand(); + sortCommand.execute(model); + + UndoCommand undoCommand = new UndoCommand(); + + try { + undoCommand.execute(model); + } catch (CommandException e) { + e.printStackTrace(); + } + + assertEquals(model.getFilteredPersonList().size(), 7); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java index 5bc11d3cdaa..d6189d204ab 100644 --- a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java @@ -7,9 +7,13 @@ import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.INVALID_ADDRESS_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_EMAIL_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_JOB_POSITION_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_NAME_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_PHONE_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_TEAM_DESC; +import static seedu.address.logic.commands.CommandTestUtil.JOB_POSITION_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.JOB_POSITION_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; @@ -18,6 +22,8 @@ import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_WHITESPACE; import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; +import static seedu.address.logic.commands.CommandTestUtil.TEAM_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.TEAM_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; @@ -26,8 +32,10 @@ import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; 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_JOB_POSITION; 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_TEAM; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; import static seedu.address.testutil.TypicalPersons.AMY; @@ -39,9 +47,11 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.JobPosition; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Team; import seedu.address.model.tag.Tag; import seedu.address.testutil.PersonBuilder; @@ -49,148 +59,145 @@ public class AddCommandParserTest { private AddCommandParser parser = new AddCommandParser(); @Test - public void parse_allFieldsPresent_success() { + public void parseCommand_allFieldsPresent_returnsAddCommand() { Person expectedPerson = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND).build(); // whitespace only preamble - assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); - + assertParseSuccess(parser, + PREAMBLE_WHITESPACE + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + + ADDRESS_DESC_BOB + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_FRIEND, + new AddCommand(expectedPerson)); // multiple tags - all accepted - Person expectedPersonMultipleTags = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) - .build(); + Person expectedPersonMultipleTags = new PersonBuilder(BOB) + .withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND).build(); assertParseSuccess(parser, - NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, new AddCommand(expectedPersonMultipleTags)); } @Test - public void parse_repeatedNonTagValue_failure() { + public void parseCommand_duplicateFields_throwsParseException() { String validExpectedPersonString = NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND; + + ADDRESS_DESC_BOB + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_FRIEND; // multiple names - assertParseFailure(parser, NAME_DESC_AMY + validExpectedPersonString, + assertParseFailure(parser, NAME_DESC_BOB + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + + ADDRESS_DESC_BOB + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_FRIEND, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); // multiple phones - assertParseFailure(parser, PHONE_DESC_AMY + validExpectedPersonString, + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + + ADDRESS_DESC_BOB + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_FRIEND, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); // multiple emails - assertParseFailure(parser, EMAIL_DESC_AMY + validExpectedPersonString, + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + EMAIL_DESC_BOB + + ADDRESS_DESC_BOB + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_FRIEND, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EMAIL)); // multiple addresses - assertParseFailure(parser, ADDRESS_DESC_AMY + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); - - // multiple fields repeated - assertParseFailure(parser, - validExpectedPersonString + PHONE_DESC_AMY + EMAIL_DESC_AMY + NAME_DESC_AMY + ADDRESS_DESC_AMY - + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME, PREFIX_ADDRESS, PREFIX_EMAIL, PREFIX_PHONE)); - - // invalid value followed by valid value - - // invalid name - assertParseFailure(parser, INVALID_NAME_DESC + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); - - // invalid email - assertParseFailure(parser, INVALID_EMAIL_DESC + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EMAIL)); - - // invalid phone - assertParseFailure(parser, INVALID_PHONE_DESC + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); - - // invalid address - assertParseFailure(parser, INVALID_ADDRESS_DESC + validExpectedPersonString, + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + ADDRESS_DESC_BOB + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_FRIEND, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); - // valid value followed by invalid value - - // invalid name - assertParseFailure(parser, validExpectedPersonString + INVALID_NAME_DESC, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME)); - - // invalid email - assertParseFailure(parser, validExpectedPersonString + INVALID_EMAIL_DESC, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_EMAIL)); - - // invalid phone - assertParseFailure(parser, validExpectedPersonString + INVALID_PHONE_DESC, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_PHONE)); + // multiple job positions + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + JOB_POSITION_DESC_BOB + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_FRIEND, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_JOB_POSITION)); - // invalid address - assertParseFailure(parser, validExpectedPersonString + INVALID_ADDRESS_DESC, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + // multiple teams + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_FRIEND, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_TEAM)); } @Test - public void parse_optionalFieldsMissing_success() { + public void parseCommand_optionalFieldMissing_returnsAddCommand() { // zero tags Person expectedPerson = new PersonBuilder(AMY).withTags().build(); - assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY, + assertParseSuccess(parser, + NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + + JOB_POSITION_DESC_AMY + TEAM_DESC_AMY, new AddCommand(expectedPerson)); } @Test - public void parse_compulsoryFieldMissing_failure() { + public void parseCommand_requiredFieldMissing_throwsParseException() { String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); // missing name prefix - assertParseFailure(parser, VALID_NAME_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, + assertParseFailure(parser, + VALID_NAME_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, expectedMessage); // missing phone prefix - assertParseFailure(parser, NAME_DESC_BOB + VALID_PHONE_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, + assertParseFailure(parser, + NAME_DESC_BOB + VALID_PHONE_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, expectedMessage); // missing email prefix - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB, + assertParseFailure(parser, + NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB, expectedMessage); // missing address prefix - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + VALID_ADDRESS_BOB, + assertParseFailure(parser, + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + VALID_ADDRESS_BOB, expectedMessage); // all prefixes missing - assertParseFailure(parser, VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB, + assertParseFailure(parser, + VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB, expectedMessage); } @Test - public void parse_invalidValue_failure() { + public void parseCommand_invalidFieldValue_throwsParseException() { // invalid name assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Name.MESSAGE_CONSTRAINTS); + + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + Name.MESSAGE_CONSTRAINTS); // invalid phone assertParseFailure(parser, NAME_DESC_BOB + INVALID_PHONE_DESC + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Phone.MESSAGE_CONSTRAINTS); + + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + Phone.MESSAGE_CONSTRAINTS); // invalid email assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + INVALID_EMAIL_DESC + ADDRESS_DESC_BOB - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Email.MESSAGE_CONSTRAINTS); + + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + Email.MESSAGE_CONSTRAINTS); // invalid address assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC - + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Address.MESSAGE_CONSTRAINTS); + + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + Address.MESSAGE_CONSTRAINTS); + + // invalid job position + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + INVALID_JOB_POSITION_DESC + TEAM_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + "Job position must start with a letter or number, found: '@'\n\n" + JobPosition.MESSAGE_CONSTRAINTS); + + // invalid team + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + JOB_POSITION_DESC_BOB + INVALID_TEAM_DESC + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + "Team name must start with a letter or number, found: '@'\n\n" + Team.MESSAGE_CONSTRAINTS); // invalid tag assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB - + INVALID_TAG_DESC + VALID_TAG_FRIEND, Tag.MESSAGE_CONSTRAINTS); + + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + INVALID_TAG_DESC + VALID_TAG_FRIEND, + Tag.MESSAGE_CONSTRAINTS); // two invalid values, only first invalid value reported - assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC, + assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC + + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Name.MESSAGE_CONSTRAINTS); // non-empty preamble assertParseFailure(parser, PREAMBLE_NON_EMPTY + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + + ADDRESS_DESC_BOB + JOB_POSITION_DESC_BOB + TEAM_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } } diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index 5a1ab3dbc0c..fadb6608f6f 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -7,13 +7,16 @@ import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.ClassifyCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; @@ -22,9 +25,11 @@ import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.SortCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; +import seedu.address.model.person.TagsContainsKeywordsPredicate; import seedu.address.testutil.EditPersonDescriptorBuilder; import seedu.address.testutil.PersonBuilder; import seedu.address.testutil.PersonUtil; @@ -88,14 +93,31 @@ public void parseCommand_list() throws Exception { assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); } + @Test + public void classifyCommand_find() throws Exception { + List keywords = Arrays.asList("friends", "owesMoney"); + List> predicates = new ArrayList<>(); + predicates.add(new TagsContainsKeywordsPredicate(List.of("friends"))); + predicates.add(new TagsContainsKeywordsPredicate(List.of("owesMoney"))); + ClassifyCommand command = (ClassifyCommand) parser.parseCommand( + ClassifyCommand.COMMAND_WORD + " t/friends t/owesMoney"); + assertEquals(new ClassifyCommand(predicates), command); + } + @Test public void parseCommand_unrecognisedInput_throwsParseException() { assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), () - -> parser.parseCommand("")); + -> parser.parseCommand("")); } @Test public void parseCommand_unknownCommand_throwsParseException() { assertThrows(ParseException.class, MESSAGE_UNKNOWN_COMMAND, () -> parser.parseCommand("unknownCommand")); } + + @Test + public void parseCommand_sort() throws Exception { + SortCommand command = (SortCommand) parser.parseCommand(SortCommand.COMMAND_WORD); + assertEquals(new SortCommand(), command); + } } diff --git a/src/test/java/seedu/address/logic/parser/ClassifyCommandParserTest.java b/src/test/java/seedu/address/logic/parser/ClassifyCommandParserTest.java new file mode 100644 index 00000000000..f02c3a9fd96 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/ClassifyCommandParserTest.java @@ -0,0 +1,56 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.ClassifyCommand; +import seedu.address.model.person.Person; +import seedu.address.model.person.TagsContainsKeywordsPredicate; + +public class ClassifyCommandParserTest { + + private ClassifyCommandParser parser = new ClassifyCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ClassifyCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_singleTag_returnsClassifyCommand() { + List> singleTagPredicates = new ArrayList<>(); + singleTagPredicates.add(new TagsContainsKeywordsPredicate(List.of("python"))); + ClassifyCommand expectedCommand = new ClassifyCommand(singleTagPredicates); + assertParseSuccess(parser, " t/python", expectedCommand); + assertParseSuccess(parser, " t/python ", expectedCommand); + } + + @Test + public void parse_multipleTagsWithAndLogic_returnsClassifyCommand() { + List> multipleTagPredicates = new ArrayList<>(); + multipleTagPredicates.add(new TagsContainsKeywordsPredicate(List.of("python"))); + multipleTagPredicates.add(new TagsContainsKeywordsPredicate(List.of("java"))); + ClassifyCommand expectedCommand = new ClassifyCommand(multipleTagPredicates); + assertParseSuccess(parser, " t/python t/java", expectedCommand); + } + + @Test + public void parse_multipleJobPositions_throwsParseException() { + assertParseFailure(parser, " j/Engineer j/Designer", + ClassifyCommand.MESSAGE_MULTIPLE_JOB_POSITIONS); + } + + @Test + public void parse_multipleTeams_throwsParseException() { + assertParseFailure(parser, " tm/Engineering tm/Design", + ClassifyCommand.MESSAGE_MULTIPLE_TEAMS); + } +} diff --git a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java index cc7175172d4..54e1e887bf6 100644 --- a/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/EditCommandParserTest.java @@ -7,9 +7,11 @@ import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.INVALID_ADDRESS_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_EMAIL_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_JOB_POSITION_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_NAME_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_PHONE_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_TEAM_DESC; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_BOB; @@ -40,8 +42,10 @@ import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.JobPosition; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.person.Team; import seedu.address.model.tag.Tag; import seedu.address.testutil.EditPersonDescriptorBuilder; @@ -87,6 +91,12 @@ public void parse_invalidValue_failure() { assertParseFailure(parser, "1" + INVALID_PHONE_DESC, Phone.MESSAGE_CONSTRAINTS); // invalid phone assertParseFailure(parser, "1" + INVALID_EMAIL_DESC, Email.MESSAGE_CONSTRAINTS); // invalid email assertParseFailure(parser, "1" + INVALID_ADDRESS_DESC, Address.MESSAGE_CONSTRAINTS); // invalid address + assertParseFailure(parser, "1" + INVALID_JOB_POSITION_DESC, + "Job position must start with a letter or number, found: '@'\n\n" + + JobPosition.MESSAGE_CONSTRAINTS); // invalid job position + assertParseFailure(parser, "1" + INVALID_TEAM_DESC, + "Team name must start with a letter or number, found: '@'\n\n" + + Team.MESSAGE_CONSTRAINTS); // invalid team assertParseFailure(parser, "1" + INVALID_TAG_DESC, Tag.MESSAGE_CONSTRAINTS); // invalid tag // invalid phone followed by valid email diff --git a/src/test/java/seedu/address/logic/parser/InterviewCommandParserTest.java b/src/test/java/seedu/address/logic/parser/InterviewCommandParserTest.java new file mode 100644 index 00000000000..1da80d147db --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/InterviewCommandParserTest.java @@ -0,0 +1,77 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.InterviewCommand; +import seedu.address.model.person.Duration; +import seedu.address.model.person.StartTime; + +public class InterviewCommandParserTest { + + private InterviewCommandParser parser = new InterviewCommandParser(); + + @Test + public void parse_validArgs_returnsInterviewCommand() { + assertParseSuccess(parser, "1 2025-04-01 10:00 30", + new InterviewCommand(INDEX_FIRST_PERSON, new StartTime("2025-04-01 10:00"), new Duration("30"))); + } + + @Test + public void parse_extraSpaces_returnsInterviewCommand() { + assertParseSuccess(parser, " 1 2025-04-01 10:15 45 ", + new InterviewCommand(INDEX_FIRST_PERSON, new StartTime("2025-04-01 10:15"), new Duration("45"))); + } + + @Test + public void parse_missingArgs_failure() { + // no index + assertParseFailure(parser, "2025-04-01 10:00 30", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE)); + + // no date + assertParseFailure(parser, "1 10-00 30", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE)); + + // no time + assertParseFailure(parser, "1 2025-04-01 30", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE)); + + // no duration + assertParseFailure(parser, "1 2025-04-01 10-00", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_invalidIndex_failure() { + assertParseFailure(parser, "0 25-04-01 10-00 30", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE)); + assertParseFailure(parser, "-1 25-04-01 10-00 30", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE)); + assertParseFailure(parser, "abc 25-04-01 10-00 30", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_invalidStartTime_failure() { + assertParseFailure(parser, "1 invalid-date 10-00 30", + StartTime.MESSAGE_CONSTRAINTS); + + assertParseFailure(parser, "1 2025-04-01 invalid 30", + StartTime.MESSAGE_CONSTRAINTS); + } + + @Test + public void parse_invalidDuration_failure() { + assertParseFailure(parser, "1 2025-04-01 10:00 -30", + Duration.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1 2025-04-01 10:00 notanumber", + Duration.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, "1 2025-04-01 10:00 17", // not a multiple of 5 + Duration.MESSAGE_CONSTRAINTS); + } +} diff --git a/src/test/java/seedu/address/logic/parser/NotesCommandParserTest.java b/src/test/java/seedu/address/logic/parser/NotesCommandParserTest.java new file mode 100644 index 00000000000..c5acafca148 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/NotesCommandParserTest.java @@ -0,0 +1,62 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_NOTES_CHARACTER_LIMIT_EXCEEDED; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.NotesCommand; +import seedu.address.model.person.Notes; + +public class NotesCommandParserTest { + + private NotesCommandParser parser = new NotesCommandParser(); + + @Test + public void parse_validNotesText_returnsNotesCommand() { + // with notes + assertParseSuccess(parser, "1 Test notes", + new NotesCommand(INDEX_FIRST_PERSON, new Notes("Test notes"))); + + // with notes containing multiple spaces + assertParseSuccess(parser, "1 Test notes with spaces", + new NotesCommand(INDEX_FIRST_PERSON, new Notes("Test notes with spaces"))); + } + + @Test + public void parse_missingIndex_throwsParseException() { + assertParseFailure(parser, "Test notes", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, NotesCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_missingNotes_throwsParseException() { + assertParseFailure(parser, "1", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, NotesCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_invalidIndex_throwsParseException() { + // negative index + assertParseFailure(parser, "-1 Test notes", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, NotesCommand.MESSAGE_USAGE)); + + // zero index + assertParseFailure(parser, "0 Test notes", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, NotesCommand.MESSAGE_USAGE)); + + // non-numeric index + assertParseFailure(parser, "abc Test notes", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, NotesCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_notesExceedsMaxLength_throwsParseException() { + String longNotes = "a".repeat(Notes.MAX_LENGTH + 1); + assertParseFailure(parser, "1 " + longNotes, + String.format(MESSAGE_NOTES_CHARACTER_LIMIT_EXCEEDED, longNotes.length())); + } +} diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java index 4256788b1a7..487e00186dd 100644 --- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java +++ b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java @@ -25,14 +25,14 @@ public class ParserUtilTest { private static final String INVALID_PHONE = "+651234"; private static final String INVALID_ADDRESS = " "; private static final String INVALID_EMAIL = "example.com"; - private static final String INVALID_TAG = "#friend"; + private static final String INVALID_TAG = "@!invalid"; private static final String VALID_NAME = "Rachel Walker"; private static final String VALID_PHONE = "123456"; private static final String VALID_ADDRESS = "123 Main Street #0505"; private static final String VALID_EMAIL = "rachel@example.com"; - private static final String VALID_TAG_1 = "friend"; - private static final String VALID_TAG_2 = "neighbour"; + private static final String VALID_TAG_1 = "C++"; + private static final String VALID_TAG_2 = "Python3.8"; private static final String WHITESPACE = " \t\r\n"; diff --git a/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java new file mode 100644 index 00000000000..bc60d1d1133 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java @@ -0,0 +1,24 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.SortCommand; + +public class SortCommandParserTest { + + private final SortCommandParser parser = new SortCommandParser(); + + @Test + public void parse_noArgs_returnsSortCommand() { + assertParseSuccess(parser, "", new SortCommand()); + } + + @Test + public void parse_extraArgs_throwsParseException() { + assertParseFailure(parser, "extra", + "Sort command does not accept any arguments.\n" + SortCommand.MESSAGE_USAGE); + } +} diff --git a/src/test/java/seedu/address/logic/parser/UndoCommandParserTest.java b/src/test/java/seedu/address/logic/parser/UndoCommandParserTest.java new file mode 100644 index 00000000000..e31ebfff6b5 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/UndoCommandParserTest.java @@ -0,0 +1,37 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.UndoCommand; + + + +public class UndoCommandParserTest { + + private UndoCommandParser parser = new UndoCommandParser(); + + @Test + public void parse_validArgs_returnsUndoCommand() { + assertParseSuccess(parser, "", new UndoCommand()); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + // no index + assertParseFailure(parser, "what ever user input", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UndoCommand.MESSAGE_USAGE)); + + // non-numeric index + assertParseFailure(parser, "abc", String.format(MESSAGE_INVALID_COMMAND_FORMAT, UndoCommand.MESSAGE_USAGE)); + + // negative index + assertParseFailure(parser, "-1", String.format(MESSAGE_INVALID_COMMAND_FORMAT, UndoCommand.MESSAGE_USAGE)); + + // zero index + assertParseFailure(parser, "0", String.format(MESSAGE_INVALID_COMMAND_FORMAT, UndoCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/model/person/DurationTest.java b/src/test/java/seedu/address/model/person/DurationTest.java new file mode 100644 index 00000000000..b66166b1481 --- /dev/null +++ b/src/test/java/seedu/address/model/person/DurationTest.java @@ -0,0 +1,41 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class DurationTest { + + @Test + public void constructor_validInput_success() { + Duration d = new Duration("30"); + assertEquals("30", d.value); + assertEquals(30, d.getDurationInMinutes()); + } + + @Test + public void constructor_blankInput_success() { + Duration d = new Duration(""); + assertEquals("", d.value); + assertEquals(0, d.getDurationInMinutes()); + } + + @Test + public void constructor_invalidFormat_throwsException() { + assertThrows(IllegalArgumentException.class, () -> new Duration("abc")); + assertThrows(IllegalArgumentException.class, () -> new Duration("17")); // not multiple of 5 + assertThrows(IllegalArgumentException.class, () -> new Duration("-10")); + } + + @Test + public void isValidDuration_variousCases() { + assertTrue(Duration.isValidDuration("15")); + assertTrue(Duration.isValidDuration("")); + assertFalse(Duration.isValidDuration("abc")); + assertFalse(Duration.isValidDuration("22")); + assertFalse(Duration.isValidDuration("-10")); + } +} diff --git a/src/test/java/seedu/address/model/person/EmailTest.java b/src/test/java/seedu/address/model/person/EmailTest.java index f08cdff0a64..b2eda9a293a 100644 --- a/src/test/java/seedu/address/model/person/EmailTest.java +++ b/src/test/java/seedu/address/model/person/EmailTest.java @@ -57,9 +57,6 @@ public void isValidEmail() { assertTrue(Email.isValidEmail("PeterJack.1190@example.com")); // period in local part assertTrue(Email.isValidEmail("PeterJack+1190@example.com")); // '+' symbol in local part assertTrue(Email.isValidEmail("PeterJack-1190@example.com")); // hyphen in local part - assertTrue(Email.isValidEmail("a@bc")); // minimal - assertTrue(Email.isValidEmail("test@localhost")); // alphabets only - assertTrue(Email.isValidEmail("123@145")); // numeric local part and domain name assertTrue(Email.isValidEmail("a1+be.d@example1.com")); // mixture of alphanumeric and special characters assertTrue(Email.isValidEmail("peter_jack@very-very-very-long-example.com")); // long domain name assertTrue(Email.isValidEmail("if.you.dream.it_you.can.do.it@example.com")); // long local part @@ -68,10 +65,10 @@ public void isValidEmail() { @Test public void equals() { - Email email = new Email("valid@email"); + Email email = new Email("valid@email.example"); // same values -> returns true - assertTrue(email.equals(new Email("valid@email"))); + assertTrue(email.equals(new Email("valid@email.example"))); // same object -> returns true assertTrue(email.equals(email)); @@ -83,6 +80,6 @@ public void equals() { assertFalse(email.equals(5.0f)); // different values -> returns false - assertFalse(email.equals(new Email("other.valid@email"))); + assertFalse(email.equals(new Email("other.valid@email.example"))); } } diff --git a/src/test/java/seedu/address/model/person/NameTest.java b/src/test/java/seedu/address/model/person/NameTest.java index 94e3dd726bd..07d6fc58cac 100644 --- a/src/test/java/seedu/address/model/person/NameTest.java +++ b/src/test/java/seedu/address/model/person/NameTest.java @@ -29,6 +29,11 @@ public void isValidName() { assertFalse(Name.isValidName(" ")); // spaces only assertFalse(Name.isValidName("^")); // only non-alphanumeric characters assertFalse(Name.isValidName("peter*")); // contains non-alphanumeric characters + assertFalse(Name.isValidName("-peter")); // starts with hyphen + assertFalse(Name.isValidName("'john")); // starts with apostrophe + assertFalse(Name.isValidName("/mary")); // starts with forward slash + assertFalse(Name.isValidName(".bob")); // starts with period + assertFalse(Name.isValidName("john@doe")); // contains @ symbol // valid name assertTrue(Name.isValidName("peter jack")); // alphabets only @@ -36,6 +41,13 @@ public void isValidName() { assertTrue(Name.isValidName("peter the 2nd")); // alphanumeric characters assertTrue(Name.isValidName("Capital Tan")); // with capital letters assertTrue(Name.isValidName("David Roger Jackson Ray Jr 2nd")); // long names + assertTrue(Name.isValidName("Anne-Marie")); // with hyphen + assertTrue(Name.isValidName("O'Connor")); // with apostrophe + assertTrue(Name.isValidName("Raj s/o Kumar")); // with s/o + assertTrue(Name.isValidName("Mary-Jane O'Connor")); // with both hyphen and apostrophe + assertTrue(Name.isValidName("Martin Luther King Jr.")); // with period + assertTrue(Name.isValidName("John Smith Sr.")); // with period + assertTrue(Name.isValidName("Dr. Jane Smith")); // with period at title } @Test diff --git a/src/test/java/seedu/address/model/person/NotesTest.java b/src/test/java/seedu/address/model/person/NotesTest.java new file mode 100644 index 00000000000..885dc9f530d --- /dev/null +++ b/src/test/java/seedu/address/model/person/NotesTest.java @@ -0,0 +1,53 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class NotesTest { + + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new Notes(null)); + } + + @Test + public void isValidNotes() { + // null notes + assertThrows(NullPointerException.class, () -> Notes.isValidNotes(null)); + + // valid notes + assertTrue(Notes.isValidNotes("")); // empty string + assertTrue(Notes.isValidNotes(" ")); // spaces only + assertTrue(Notes.isValidNotes("Good candidate")); // normal notes + assertTrue(Notes.isValidNotes("Very detailed notes with multiple sentences. Shows promise in leadership.")); + assertTrue(Notes.isValidNotes("a".repeat(Notes.MAX_LENGTH))); // exactly maximum characters + + // invalid notes + assertFalse(Notes.isValidNotes("a".repeat(Notes.MAX_LENGTH + 1))); // exceeds maximum length + } + + @Test + public void equals() { + Notes notes = new Notes("Test notes"); + + // same object -> returns true + assertTrue(notes.equals(notes)); + + // same value -> returns true + Notes sameNotes = new Notes("Test notes"); + assertTrue(notes.equals(sameNotes)); + + // null -> returns false + assertFalse(notes.equals(null)); + + // different type -> returns false + assertFalse(notes.equals(5)); + + // different value -> returns false + Notes differentNotes = new Notes("Different notes"); + assertFalse(notes.equals(differentNotes)); + } +} diff --git a/src/test/java/seedu/address/model/person/PersonTest.java b/src/test/java/seedu/address/model/person/PersonTest.java index 31a10d156c9..facdda15f62 100644 --- a/src/test/java/seedu/address/model/person/PersonTest.java +++ b/src/test/java/seedu/address/model/person/PersonTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_ALICE; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; @@ -32,23 +33,30 @@ public void isSamePerson() { // null -> returns false assertFalse(ALICE.isSamePerson(null)); - // same name, all other attributes different -> returns true + // same name, all other attributes different -> returns false + // There might be candidates with the same name. Person editedAlice = new PersonBuilder(ALICE).withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB) .withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND).build(); + assertFalse(ALICE.isSamePerson(editedAlice)); + + // same email, all other attributes different -> returns true + editedAlice = new PersonBuilder(BOB).withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_ALICE) + .withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND).build(); assertTrue(ALICE.isSamePerson(editedAlice)); - // different name, all other attributes same -> returns false + // different name, all other attributes same -> returns true editedAlice = new PersonBuilder(ALICE).withName(VALID_NAME_BOB).build(); - assertFalse(ALICE.isSamePerson(editedAlice)); + assertTrue(ALICE.isSamePerson(editedAlice)); - // name differs in case, all other attributes same -> returns false - Person editedBob = new PersonBuilder(BOB).withName(VALID_NAME_BOB.toLowerCase()).build(); - assertFalse(BOB.isSamePerson(editedBob)); + // email differs in case, all other attributes same -> returns true + // This is because the email address is not case-sensitive. + Person editedBob = new PersonBuilder(BOB).withEmail(VALID_EMAIL_BOB.toUpperCase()).build(); + assertTrue(BOB.isSamePerson(editedBob)); - // name has trailing spaces, all other attributes same -> returns false + // name has trailing spaces, all other attributes same -> returns true String nameWithTrailingSpaces = VALID_NAME_BOB + " "; editedBob = new PersonBuilder(BOB).withName(nameWithTrailingSpaces).build(); - assertFalse(BOB.isSamePerson(editedBob)); + assertTrue(BOB.isSamePerson(editedBob)); } @Test @@ -93,7 +101,12 @@ public void equals() { @Test public void toStringMethod() { String expected = Person.class.getCanonicalName() + "{name=" + ALICE.getName() + ", phone=" + ALICE.getPhone() - + ", email=" + ALICE.getEmail() + ", address=" + ALICE.getAddress() + ", tags=" + ALICE.getTags() + "}"; + + ", email=" + ALICE.getEmail() + ", address=" + ALICE.getAddress() + + ", jobPosition=" + ALICE.getJobPosition() + ", team=" + ALICE.getTeam() + + ", tags=" + ALICE.getTags() + ", notes=" + ALICE.getNotes() + + ", interview time=" + ALICE.getStartTime() + + ", duration=" + ALICE.getDuration() + "}"; + assertEquals(expected, ALICE.toString()); } } diff --git a/src/test/java/seedu/address/model/person/StartTimeTest.java b/src/test/java/seedu/address/model/person/StartTimeTest.java new file mode 100644 index 00000000000..ca8900bed32 --- /dev/null +++ b/src/test/java/seedu/address/model/person/StartTimeTest.java @@ -0,0 +1,48 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +public class StartTimeTest { + + @Test + public void constructor_validInput_success() { + StartTime st = new StartTime("2025-04-01 10:15"); + assertEquals("2025-04-01 10:15", st.value); + assertEquals(LocalDateTime.of(2025, 4, 1, 10, 15), st.getParsedStartTime()); + } + + @Test + public void constructor_nullInput_success() { + StartTime st = new StartTime(null); + assertEquals("", st.value); + assertNull(st.getParsedStartTime()); + } + + @Test + public void constructor_invalidFormat_throwsException() { + assertThrows(IllegalArgumentException.class, () -> new StartTime("2025/04/01 10:15")); + } + + @Test + public void constructor_minuteNotMultipleOfFive_throwsException() { + assertThrows(IllegalArgumentException.class, () -> new StartTime("2025-04-01 10:07")); + } + + @Test + public void isValidStartTime_variousCases() { + assertTrue(StartTime.isValidStartTime("2025-12-31 23:55")); + assertFalse(StartTime.isValidStartTime("2025-04-01 10-15")); // wrong format + assertFalse(StartTime.isValidStartTime("2025-04-01 10:03")); // invalid minute + assertTrue(StartTime.isValidStartTime(null)); + assertTrue(StartTime.isValidStartTime("")); + } + +} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java index 83b11331cdb..c3c4600bf85 100644 --- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java +++ b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java @@ -14,20 +14,30 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.JobPosition; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.person.Team; public class JsonAdaptedPersonTest { private static final String INVALID_NAME = "R@chel"; private static final String INVALID_PHONE = "+651234"; private static final String INVALID_ADDRESS = " "; private static final String INVALID_EMAIL = "example.com"; - private static final String INVALID_TAG = "#friend"; + private static final String INVALID_JOB_POSITION = "@invalid job!"; + private static final String INVALID_TEAM = "@invalid team!"; + private static final String INVALID_TAG = "@!friend"; private static final String VALID_NAME = BENSON.getName().toString(); private static final String VALID_PHONE = BENSON.getPhone().toString(); private static final String VALID_EMAIL = BENSON.getEmail().toString(); private static final String VALID_ADDRESS = BENSON.getAddress().toString(); + private static final String VALID_JOB_POSITION = BENSON.getJobPosition().toString(); + private static final String VALID_TEAM = BENSON.getTeam().toString(); + private static final String VALID_START_TIME = BENSON.getStartTime().toString(); + private static final String VALID_DURATION = BENSON.getDuration().toString(); + private static final String VALID_NOTES = BENSON.getNotes().toString(); + private static final List VALID_TAGS = BENSON.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList()); @@ -41,14 +51,18 @@ public void toModelType_validPersonDetails_returnsPerson() throws Exception { @Test public void toModelType_invalidName_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_JOB_POSITION, VALID_TEAM, VALID_NOTES, VALID_TAGS, + VALID_START_TIME, VALID_DURATION); + String expectedMessage = Name.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullName_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_JOB_POSITION, VALID_TEAM, VALID_NOTES, VALID_TAGS, VALID_START_TIME, VALID_DURATION); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -56,14 +70,19 @@ public void toModelType_nullName_throwsIllegalValueException() { @Test public void toModelType_invalidPhone_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_JOB_POSITION, VALID_TEAM, VALID_NOTES, VALID_TAGS, + VALID_START_TIME, VALID_DURATION); + String expectedMessage = Phone.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullPhone_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, + VALID_JOB_POSITION, VALID_TEAM, VALID_NOTES, VALID_TAGS, VALID_START_TIME, VALID_DURATION); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -71,14 +90,19 @@ public void toModelType_nullPhone_throwsIllegalValueException() { @Test public void toModelType_invalidEmail_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, + VALID_JOB_POSITION, VALID_TEAM, VALID_NOTES, VALID_TAGS, + VALID_START_TIME, VALID_DURATION); + String expectedMessage = Email.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullEmail_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, + VALID_JOB_POSITION, VALID_TEAM, VALID_NOTES, VALID_TAGS, VALID_START_TIME, VALID_DURATION); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -86,24 +110,71 @@ public void toModelType_nullEmail_throwsIllegalValueException() { @Test public void toModelType_invalidAddress_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, + VALID_JOB_POSITION, VALID_TEAM, VALID_NOTES, VALID_TAGS, + VALID_START_TIME, VALID_DURATION); + String expectedMessage = Address.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, + VALID_JOB_POSITION, VALID_TEAM, VALID_NOTES, VALID_TAGS, VALID_START_TIME, VALID_DURATION); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } + @Test + public void toModelType_invalidJobPosition_throwsIllegalValueException() { + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + INVALID_JOB_POSITION, VALID_TEAM, VALID_NOTES, VALID_TAGS, + VALID_START_TIME, VALID_DURATION); + + String expectedMessage = JobPosition.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_nullJobPosition_throwsIllegalValueException() { + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + null, VALID_TEAM, VALID_NOTES, VALID_TAGS, VALID_START_TIME, VALID_DURATION); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, JobPosition.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_invalidTeam_throwsIllegalValueException() { + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_JOB_POSITION, INVALID_TEAM, VALID_NOTES, VALID_TAGS, + VALID_START_TIME, VALID_DURATION); + + String expectedMessage = Team.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_nullTeam_throwsIllegalValueException() { + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_JOB_POSITION, null, VALID_NOTES, VALID_TAGS, VALID_START_TIME, VALID_DURATION); + + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Team.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + @Test public void toModelType_invalidTags_throwsIllegalValueException() { List invalidTags = new ArrayList<>(VALID_TAGS); invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_JOB_POSITION, VALID_TEAM, VALID_NOTES, invalidTags, + VALID_START_TIME, VALID_DURATION); + assertThrows(IllegalValueException.class, person::toModelType); } diff --git a/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java b/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java index ed0a413526a..2ff9c00a507 100644 --- a/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java +++ b/src/test/java/seedu/address/storage/JsonUserPrefsStorageTest.java @@ -73,7 +73,7 @@ public void readUserPrefs_extraValuesInFile_extraValuesIgnored() throws DataLoad private UserPrefs getTypicalUserPrefs() { UserPrefs userPrefs = new UserPrefs(); userPrefs.setGuiSettings(new GuiSettings(1000, 500, 300, 100)); - userPrefs.setAddressBookFilePath(Paths.get("addressbook.json")); + userPrefs.setAddressBookFilePath(Paths.get("recruitintel.json")); return userPrefs; } diff --git a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java index 4584bd5044e..d14c5fb1c03 100644 --- a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java +++ b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java @@ -7,9 +7,11 @@ import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.JobPosition; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Team; import seedu.address.model.tag.Tag; /** @@ -36,6 +38,8 @@ public EditPersonDescriptorBuilder(Person person) { descriptor.setPhone(person.getPhone()); descriptor.setEmail(person.getEmail()); descriptor.setAddress(person.getAddress()); + descriptor.setJobPosition(person.getJobPosition()); + descriptor.setTeam(person.getTeam()); descriptor.setTags(person.getTags()); } @@ -71,6 +75,22 @@ public EditPersonDescriptorBuilder withAddress(String address) { return this; } + /** + * Sets the {@code JobPosition} of the {@code EditPersonDescriptor} that we are building. + */ + public EditPersonDescriptorBuilder withJobPosition(String jobPosition) { + descriptor.setJobPosition(new JobPosition(jobPosition)); + return this; + } + + /** + * Sets the {@code Team} of the {@code EditPersonDescriptor} that we are building. + */ + public EditPersonDescriptorBuilder withTeam(String team) { + descriptor.setTeam(new Team(team)); + return this; + } + /** * Parses the {@code tags} into a {@code Set} and set it to the {@code EditPersonDescriptor} * that we are building. diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java index 6be381d39ba..32f16b13909 100644 --- a/src/test/java/seedu/address/testutil/PersonBuilder.java +++ b/src/test/java/seedu/address/testutil/PersonBuilder.java @@ -5,9 +5,11 @@ import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.JobPosition; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Team; import seedu.address.model.tag.Tag; import seedu.address.model.util.SampleDataUtil; @@ -20,11 +22,15 @@ public class PersonBuilder { public static final String DEFAULT_PHONE = "85355255"; public static final String DEFAULT_EMAIL = "amy@gmail.com"; public static final String DEFAULT_ADDRESS = "123, Jurong West Ave 6, #08-111"; + public static final String DEFAULT_JOB_POSITION = "Software Engineer"; + public static final String DEFAULT_TEAM = "Engineering"; private Name name; private Phone phone; private Email email; private Address address; + private JobPosition jobPosition; + private Team team; private Set tags; /** @@ -35,6 +41,8 @@ public PersonBuilder() { phone = new Phone(DEFAULT_PHONE); email = new Email(DEFAULT_EMAIL); address = new Address(DEFAULT_ADDRESS); + jobPosition = new JobPosition(DEFAULT_JOB_POSITION); + team = new Team(DEFAULT_TEAM); tags = new HashSet<>(); } @@ -46,6 +54,8 @@ public PersonBuilder(Person personToCopy) { phone = personToCopy.getPhone(); email = personToCopy.getEmail(); address = personToCopy.getAddress(); + jobPosition = personToCopy.getJobPosition(); + team = personToCopy.getTeam(); tags = new HashSet<>(personToCopy.getTags()); } @@ -89,8 +99,24 @@ public PersonBuilder withEmail(String email) { return this; } + /** + * Sets the {@code JobPosition} of the {@code Person} that we are building. + */ + public PersonBuilder withJobPosition(String jobPosition) { + this.jobPosition = new JobPosition(jobPosition); + return this; + } + + /** + * Sets the {@code Team} of the {@code Person} that we are building. + */ + public PersonBuilder withTeam(String team) { + this.team = new Team(team); + return this; + } + public Person build() { - return new Person(name, phone, email, address, tags); + return new Person(name, phone, email, address, jobPosition, team, tags); } } diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java index 90849945183..f35e5f29451 100644 --- a/src/test/java/seedu/address/testutil/PersonUtil.java +++ b/src/test/java/seedu/address/testutil/PersonUtil.java @@ -2,9 +2,11 @@ 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_JOB_POSITION; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TEAM; import java.util.Set; @@ -34,8 +36,10 @@ public static String getPersonDetails(Person person) { sb.append(PREFIX_PHONE + person.getPhone().value + " "); sb.append(PREFIX_EMAIL + person.getEmail().value + " "); sb.append(PREFIX_ADDRESS + person.getAddress().value + " "); + sb.append(PREFIX_JOB_POSITION + person.getJobPosition().value + " "); + sb.append(PREFIX_TEAM + person.getTeam().value + " "); person.getTags().stream().forEach( - s -> sb.append(PREFIX_TAG + s.tagName + " ") + s -> sb.append(PREFIX_TAG + s.tagName + " ") ); return sb.toString(); } @@ -49,6 +53,9 @@ public static String getEditPersonDescriptorDetails(EditPersonDescriptor descrip descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" ")); descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_EMAIL).append(email.value).append(" ")); descriptor.getAddress().ifPresent(address -> sb.append(PREFIX_ADDRESS).append(address.value).append(" ")); + descriptor.getJobPosition().ifPresent(jobPosition -> sb.append(PREFIX_JOB_POSITION).append(jobPosition.value) + .append(" ")); + descriptor.getTeam().ifPresent(team -> sb.append(PREFIX_TEAM).append(team.value).append(" ")); if (descriptor.getTags().isPresent()) { Set tags = descriptor.getTags().get(); if (tags.isEmpty()) { diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java index fec76fb7129..b49cc5b41de 100644 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ b/src/test/java/seedu/address/testutil/TypicalPersons.java @@ -25,35 +25,47 @@ public class TypicalPersons { public static final Person ALICE = new PersonBuilder().withName("Alice Pauline") .withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com") - .withPhone("94351253") + .withPhone("94351253").withJobPosition("Software Engineer").withTeam("Engineering") .withTags("friends").build(); public static final Person BENSON = new PersonBuilder().withName("Benson Meier") .withAddress("311, Clementi Ave 2, #02-25") .withEmail("johnd@example.com").withPhone("98765432") + .withJobPosition("Product Manager").withTeam("Product") .withTags("owesMoney", "friends").build(); public static final Person CARL = new PersonBuilder().withName("Carl Kurz").withPhone("95352563") - .withEmail("heinz@example.com").withAddress("wall street").build(); + .withEmail("heinz@example.com").withAddress("wall street") + .withJobPosition("UI Designer").withTeam("Design").build(); public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533") - .withEmail("cornelia@example.com").withAddress("10th street").withTags("friends").build(); + .withEmail("cornelia@example.com").withAddress("10th street") + .withJobPosition("Data Scientist").withTeam("Data") + .withTags("friends").build(); public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("9482224") - .withEmail("werner@example.com").withAddress("michegan ave").build(); + .withEmail("werner@example.com").withAddress("michegan ave") + .withJobPosition("DevOps Engineer").withTeam("Infrastructure").build(); public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427") - .withEmail("lydia@example.com").withAddress("little tokyo").build(); + .withEmail("lydia@example.com").withAddress("little tokyo") + .withJobPosition("Backend Engineer").withTeam("Engineering").build(); public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("9482442") - .withEmail("anna@example.com").withAddress("4th street").build(); + .withEmail("anna@example.com").withAddress("4th street") + .withJobPosition("Frontend Engineer").withTeam("Engineering").build(); // Manually added public static final Person HOON = new PersonBuilder().withName("Hoon Meier").withPhone("8482424") - .withEmail("stefan@example.com").withAddress("little india").build(); + .withEmail("stefan@example.com").withAddress("little india") + .withJobPosition("QA Engineer").withTeam("QA").build(); public static final Person IDA = new PersonBuilder().withName("Ida Mueller").withPhone("8482131") - .withEmail("hans@example.com").withAddress("chicago ave").build(); + .withEmail("hans@example.com").withAddress("chicago ave") + .withJobPosition("Security Engineer").withTeam("Security").build(); // Manually added - Person's details found in {@code CommandTestUtil} public static final Person AMY = new PersonBuilder().withName(VALID_NAME_AMY).withPhone(VALID_PHONE_AMY) - .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).withTags(VALID_TAG_FRIEND).build(); + .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) + .withJobPosition("Software Engineer").withTeam("Engineering") + .withTags(VALID_TAG_FRIEND).build(); public static final Person BOB = new PersonBuilder().withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) - .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND) - .build(); + .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) + .withJobPosition("Product Manager").withTeam("Product") + .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER