diff --git a/.gitignore b/.gitignore index 71c9194e8bd..1e01f356e88 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ src/main/resources/docs/ /preferences.json /*.log.* +# Csv export files +*.csv + # Test sandbox files src/test/data/sandbox/ diff --git a/LICENSE b/LICENSE index 39b3478982c..b07be9d02b1 100644 --- a/LICENSE +++ b/LICENSE @@ -2,11 +2,11 @@ MIT License Copyright (c) 2016 Software Engineering Education - FOSS Resources -Permission is hereby granted, free of charge, to any person obtaining a copy +Permission is hereby granted, free of charge, to any applicant obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is +copies of the Software, and to permit applicants to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all diff --git a/README.md b/README.md index 13f5c77403f..c220a6af3bf 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,27 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![Java CI](https://github.com/AY2122S2-CS2103-W17-4/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2122S2-CS2103-W17-4/tp/actions/workflows/gradle.yml) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. +* This application is designed for **Recruiters for Tech companies** who have many candidates to track. + + +* Usage: + * Schedule interviews with potential applicants + * Track job candidates through the hiring process + + +* The application is optimised for Command Line Interface (_CLI_) users. However, a graphic user interface (_GUI_) is + also offered. + + +* This is done to allow power users to accomplish tasks much quicker through the use of commands + + +* It is named `HireLah` because it should make the recruiter utter these words when they are using it `"aiyo just + HireLah!"` + + +* For the detailed documentation of this project, see the **[HireLah Product Website](https://ay2122s2-cs2103-w17-4.github.io/tp/)**. + + +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org) diff --git a/build.gradle b/build.gradle index be2d2905dde..4c6f868855a 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,11 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'HireLah.jar' +} + +run { + enableAssertions = true } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..3d5ab9ad45a 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,55 @@ title: About Us We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg). -You can reach us at the email `seer[at]comp.nus.edu.sg` +## Project Team -## Project team +### Bryan Ong -### John Doe + - +[[github](https://github.com/likeabowx)] +[[portfolio](team/likeabowx.md)] -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] - -* Role: Project Advisor +* Role: Developer +* Responsibilities: Integration of code, and in charge of `Filter` commands -### Jane Doe +### Chong Kok Leong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/SethCKL)] +[[portfolio](team/sethckl.md)] -* Role: Team Lead -* Responsibilities: UI +* Role: Developer +* Responsibilities: Project Management, Documentation, and in charge of `Applicant` component -### Johnny Doe +### Le Nguyen Quang Dang Khoa - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](https://github.com/khoahre123)] +[[portfolio](team/khoahre123.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Deliverables and deadlines and in charge of +`help` commands -### Jean Doe +### Lee Yi Hern - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/YiHern-Lee)] +[[portfolio](team/yihern-lee.md)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Code quality and in charge of `Position` component -### James Doe +### Tan Wei Howe - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/goalfix)] +[[portfolio](team/goalfix.md)] * Role: Developer -* Responsibilities: UI +* Responsibilities: Git expert, Scheduling and tracking, In charge of `Interview` component. diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 46eae8ee565..ccc5d4ffcc9 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -9,7 +9,7 @@ title: Developer Guide ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* This project is based on the [AddressBook-Level3](https://github.com/se-edu/addressbook-level3) project by [SE-EDU](https://se-education.org). -------------------------------------------------------------------------------------------------------------------- @@ -23,7 +23,7 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md).
-:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](https://github.com/se-edu/addressbook-level3/tree/master/docs/diagrams/) folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. +:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](https://github.com/AY2122S2-CS2103-W17-4/tp/tree/master/docs/diagrams/) folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams.
### Architecture @@ -36,7 +36,7 @@ Given below is a quick overview of main components and how they interact with ea **Main components of the architecture** -**`Main`** has two classes called [`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). It is responsible for, +**`Main`** has two classes called [`Main`](https://github.com/AY2122S2-CS2103-W17-4/tp/blob/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2122S2-CS2103-W17-4/tp/blob/master/src/main/java/seedu/address/MainApp.java). It is responsible for, * At app launch: Initializes the components in the correct sequence, and connects them up with each other. * At shut down: Shuts down the components and invokes cleanup methods where necessary. @@ -52,7 +52,7 @@ The rest of the App consists of four components. **How the architecture components interact with each other** -The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete -a 1`. @@ -69,38 +69,42 @@ 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/AY2122S2-CS2103-W17-4/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 consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `ApplicantListPanel`, `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/AY2122S2-CS2103-W17-4/tp/blob/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2122S2-CS2103-W17-4/tp/blob/master/src/main/resources/view/MainWindow.fxml) + +Note that `ApplicantListPanel`, `PositionListPanel`, and `InterviewListPanel` will all exists simultaneously in the `UI` component, but only one will be visible to the user as controlled by tabs in `MainWindow`. The `UI` component, * executes user commands using the `Logic` component. +* changes the selected tab automatically according to the `DataType` in `CommandResult` from the `Logic` component. * listens for changes to `Model` data so that the UI can be updated with the modified data. * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +* depends on some classes in the `Model` component, as it displays the `Applicant`, `Position` and `Interview` objects residing in the `Model`. ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2122S2-CS2103-W17-4/tp/blob/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: How the `Logic` component works: -1. When `Logic` is called upon to execute a command, it uses the `AddressBookParser` class to parse the user command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `AddCommand`) which is executed by the `LogicManager`. -1. The command can communicate with the `Model` when it is executed (e.g. to add a person). +1. When `Logic` is called upon to execute a command, it uses the `HireLahParser` class to parse the user command. +1. In the case of commands that is common to all data types (e.g. `add`, `edit`, `delete`, `list`), an intermediate parser may be used to select the specific parser for the data type. +1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `AddApplicantCommand`) which is executed by the `LogicManager`. +1. The command can communicate with the `Model` when it is executed (e.g. to add a applicant). 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. -The Sequence Diagram below illustrates the interactions within the `Logic` component for the `execute("delete 1")` API call. +The Sequence Diagram below illustrates the interactions within the `Logic` component for the `execute("delete -a 1")` API call. -![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) +![Interactions Inside the Logic Component for the `delete -a 1` Command](images/DeleteSequenceDiagram.png)
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
@@ -110,43 +114,54 @@ Here are the other classes in `Logic` (omitted from the class diagram above) tha How the parsing works: -* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. +* When called upon to parse a user command, the `HireLahParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `HireLahParser` returns back as a `Command` object. * All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) +**API** : [`Model.java`](https://github.com/AY2122S2-CS2103-W17-4/tp/blob/master/src/main/java/seedu/address/model/Model.java) +
+In the `Model`, `ModelManager` contains three different `DataType` – `Applicant`, `Position` and `Interview`, each with their own `UniqueXYZList` contained in `HireLah`. The class diagrams for each `DataType` are separated below for better clarity. -The `Model` component, +`Applicant` class diagram: -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. -* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. -* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) + -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+`Position` class diagram: - + -
+`Interview` class diagram: + + + +Note that the `Interview` class contains `Applicant` and `Position`. + + +The `Model` component, + +* stores all the data i.e., all `Applicant`, `Position`, and `Interview` objects (which are contained in `UniqueApplicantList`, `UniquePositionList`, and `UniqueInterviewList` objects respectively). +* stores the currently 'selected' `Applicant`, `Position`, and `Interview` objects (e.g., results after a list command with filter applied) as a separate _filtered_ list which is exposed to outsiders as unmodifiable Java's Observable List (i.e., `ObservableList`, `ObservableList` and `ObservableList` for the different types) 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) ### 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/AY2122S2-CS2103-W17-4/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. -* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). +* can save both HireLah data, which consists of `Applicants`, `Interviews` and `Positions`; and user preference data in json format, and read them back into corresponding objects. +* inherits from both `HireLahStorage` and `UserPrefsStorage`, 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`) ### Common classes -Classes used by multiple components are in the `seedu.addressbook.commons` package. +Classes used by multiple components are in the [`seedu.address.commons`](https://github.com/AY2122S2-CS2103-W17-4/tp/tree/master/src/main/java/seedu/address/commons) package. -------------------------------------------------------------------------------------------------------------------- @@ -154,95 +169,257 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### Applicant 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: +An applicant in HireLah is represented by `Applicant`. `Applicant` is implemented by refactoring `Persons`. +Additionally, `Applicant` implements two new attributes which are represented by the following two new classes: -* `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. +* `Gender` — M refers to male, and F refers to female. Only the value M or F is allowed. +* `Age` —  Numerical representation of the age of the applicant. Only values with two digits or more are allowed. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +`Gender` and `Age` class highly resemble other existing attribute classes such as `Address`, `Email`, `Name`, and +`Phone`. -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +These classes are contained in the `applicant` package which belongs to the `model` package. -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. +Applicant is implemented this way as for HireLah, we require new attributes such as `Gender` and `Age` to aid in the +recruitment process. the `Person` class did not contain such attributes. -![UndoRedoState0](images/UndoRedoState0.png) +Adding Gender and Age as tags using the existing functionality is not ideal as we do not want these attributes to be +optional. -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. +A new `Applicant` class had to be created to support the functionality. It is also not ideal to keep the existing +`Person` class as it should not be instantiated by users in HireLah. -![UndoRedoState1](images/UndoRedoState1.png) +Hence it made sense to refactor `Person` to `Applicant` and to extend and build on the existing functionalities to +support the needs of HireLah. -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`. +### Position feature -![UndoRedoState2](images/UndoRedoState2.png) +#### Implementation -
: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`. +A position in HireLah is represented by `Position`. `Position` is implemented with the following attributes: +* `PositionName` —  refers to the name of the job opening. + Can allow any characters, but must have at least one alphanumeric character. Length is restricted to a maximum of 100 characters. +* `Description` —  refers to the description of the position. + Can allow any characters, but must have at least one alphanumeric character. Length is restricted to a maximum of 200 characters. +* `PositionOpenings` —  refers to the number of openings in the position. Can allow only numbers of 1 to 5 digits. +* `PositionOffers` —  refers to the number of outstanding offers handed out for the position. + Number of offers is initialized as 0 when a position is created. Number of offers cannot be directly mutated, and is only altered through commands of `pass`, `accept`, `reject`. +* `Set` —  refers to a set of requirements that is required for an `Applicant` to be considered for the `Position`. + There can be any number of requirements for the `Position`. + +These classes are contained in the `position` package which belongs to the `model` package. -
+Position is implemented this way as for HireLah, as we need these informations, in order to aid recruiters +in keeping track of crucial job-related information in the hiring process. -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. +#### Design considerations: -![UndoRedoState3](images/UndoRedoState3.png) +#### Aspect: Ensuring that number of applicants offered a job does not exceed the number of job openings +* **Alternative 1 (current choice):** `PositionOffers` is implemented in a way that disallow users from directly mutating the underlying value. +`PositionOffers` is only mutated through various commands listed under section **Tracking Interview Status**. + * Pros: Number of `PositionOffers` is guaranteed to tally with number of "passed interviews". + * Cons: Difficulty in implementing due to coupling with the `Interview` class. Actions that mutate `Interview` may cause changes to `PositionOffers`. + It will also be more difficult for users to correct the erroneous commands, as they cannot directly decrement or increment `PositionOffers`. + +* **Alternative 2** Allowing users to manually set their own number of offers. + * Pros: Greater flexibility for users to update and keep track of the number of offers handed out. + * Cons: Users will have to exercise their own diligence in ensuring that number of offers handed out tallies with the number + of "passed interview". -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather -than attempting to perform the undo. +### Tracking Interview Status -
+#### Implementation -The following sequence diagram shows how the undo operation works: +Currently, there are 5 possible status for interviews which represents where an applicant is in the hiring pipeline. +* `Pending` - Interview has been created / scheduled, applicant yet to go for interview. +* `Passed - waiting for applicant` - Applicant has passed the interview. A job **offer is automatically extended** to the applicant at this stage. +* `Failed` - Applicant has failed the interview. +* `Accepted` - Applicant has accepted the job offer. Applicant job role will be updated in Applicants tab. +* `Rejected` - Applicant has rejected the job offer. -![UndoSequenceDiagram](images/UndoSequenceDiagram.png) +The **activity diagram** below shows the workflows between different interview status and corresponding updates to `Position` +and `Applicant` classes. -
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +![Activity diagram between different interview status](images/InterviewStatus.png) -
+#### Design considerations: -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. +#### Aspect: When an applicant is considered to be matched with a job: -
: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. +* **Alternative 1 (current choice):** An applicant must accept a passed interview before the applicant is hired for that position. + * Pros: A more accurate modelling of real-world hiring processes, whereby an applicant may actually be accepted for multiple roles, and has to choose one role to accept. + * Cons: Have to track number of position offers currently given out with respect to number of open positions, + preventing a scenario where multiple people accept the offer but there is a shortage of actual position openings. + More complex model which may be bug-prone. -
-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. +* **Alternative 2:** An applicant is considered to be hired after passing the interview. + * Pros: A simplified way of matching, reduces complexity of interview and coupling between Interview and Positions. + * Cons: Does not model real-world interview processes accurately, forces applicant to accept the first job which they pass the interview for. + +#### Aspect: Number of interviews per applicant allowed for each unique role: + +* **Alternative 1 (current choice):** An applicant can only schedule one interview for each unique position they apply for. + * Pros: A simplified model that reduces complexity of when to hand out job offers, reducing bugs. + * Cons: May not model the real-world hiring process accurately where some roles require multiple interviews. + -![UndoRedoState4](images/UndoRedoState4.png) +* **Alternative 2:** An applicant can schedule multiple interviews for a unique position they apply for. + * Pros: A more accurate modelling of real-world hiring processes. + * Cons: Increased complexity of hiring process. + Need to keep track of different number of interviews required for every unique position and where each applicant is + at which stage e.g "Finished HR interview" / "Finished Online Assessment", which may result in more bugs. -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. +### Adding of Data -![UndoRedoState5](images/UndoRedoState5.png) +#### Implementation -The following activity diagram summarizes what happens when a user executes a new command: +Adding of different data types is currently done through `ModelManger`, which implements the methods in interface `Model`. +There are 3 levels to the parsing of the add command from user input. +1. `HireLahParser` identifies it as an `add` command. +2. `AddCommandParser` identifies the exact data type that is to be added, through the `flag` of the user input. +3. `AddXYZCommandParser` identifies the fields to be added for the specific datatype, and creates and `AddXYZCommand`. - +The **sequence diagram** below shows how the parsing of `add -i` works. +Note that the lifeline for `AddCommandParser` and `AddInterviewCommandParser` should end at the destroy marker (X) but due to +a limitation of PlantUML, the lifeline reaches the end of diagram. Logic for execution of `AddInterviewCommand` is omitted. + +![Add parser for interview](images/AddParser.png) #### Design considerations: -**Aspect: How undo & redo executes:** +#### Aspect: How to add different data types: + +* **Alternative 1 (current choice):** Have a general add command. + * Pros: User-friendly since users only have to remember a singular command. + * Cons: Requires additional levels of parsers to be created. + +* **Alternative 2:** An individual command for each data type that can be added (eg. `addappl`, `addintvw`) + * Pros: Fewer levels of parsers is required. + * Cons: We must ensure that the implementation of each individual command are correct. Many commands to remember for a new user. + +### Deleting of Data + +#### Implementation +The implementation of deleting data is similar to adding data, where deleting of different data types is done through `ModelManger`, which implements the methods in the `Model` interface. + +The parsing of a delete command from user input is also done through the 3 levels system, with `HireLahParser`, `DeleteCommandParser`, and `DeleteXYZCommandParser` which eventually creates the `DeleteXYZCommand`. + +However, when deleting an applicant or a position, an additional step of cascading to delete interview is required. Since every interview is associated with an applicant and a position, we cannot have an interview exist without the corresponding applicant or position. +Hence, it is important to delete the associated interview(s) when deleting an applicant or a position. + +#### Design considerations: + +#### Aspect: How to cascade when deleting applicant/position to delete interview: + +* **Alternative 1 (current choice):** Loop through all interviews in `DeleteXYZCommand` + * Pros: Less coupling as a data type does not store another data type as an attribute. + * Cons: May be less efficient as we have to loop through the whole list of interviews everytime when deleting applicant/position. + + +* **Alternative 2:** Keep relevant list of interviews for each applicant and position. + * Pros: More efficient when deleting since all the associated interviews are already available. + * Cons: Increased coupling between applicant, position, and interview which make it more bug-prone. + +### Filtering of Data + +#### Implementation + +The implementation of filtering data is done as an extension of the `list -X` command, which takes in optional parameters that will trigger the filtering of data to display if given. The filtering of data is done similar to the `find` command in AB3, which is now deprecated in HireLah. It applies a predicate to the `filteredXYZ` filtered lists in the `ModelManager`, which the `UI` will pick up and display the latest filtered list of the data to the user. + +To support different filters for different data types, each filter is a predicate class in the `Model` component. For example, to support filtering applicants by gender, there is a [`ApplicantGenderPredicate`](https://github.com/AY2122S2-CS2103-W17-4/tp/blob/master/src/main/java/seedu/address/model/applicant/ApplicantGenderPredicate.java) in the `Model` component under `applicant`. The predicate implements Java's `Predicate` interface for filtered lists. + +Here is the sequence diagram for a filter command: + +The *Sequence Diagram* below illustrates the interactions within the classes for the execution of `list -a f/name a/Bob` command. + + + +#### Design considerations: + +#### Aspect: Should the filter feature be a separate command by itself? + +* **Alternative 1:** Implement filter as a separate `filter -X` command. + * Pros: May be more intuitive for new users to pick up. Can also potentially make the parsing of filter-related arguments less complicated. + * Cons: Multiple commands doing similar things because `filter` is essentially `list` with different predicates applied to the filtered lists. Listing all data is also a predicate itself. + -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +* **Alternative 2 (current choice):** Implement filter as part of the `list -X` command (by taking in more parameters). + * Pros: No two commands doing the similar things, which may lead to chunks of repeated code under the two commands. + * Cons: May be confusing for new users, need to explain it well in user guide and help window. Also, will have to parse filter-related arguments together with other arguments in `list -X` command (such as for sorting), which may cause the parsing to be more complicated. -* **Alternative 2:** Individual command knows how to undo/redo by - itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. +### Sorting of Data -_{more aspects and alternatives to be added}_ +#### Implementation +The implementation of sorting data is done as an extension of the `list -X` command, which takes in optional +parameters that will trigger the sorting of data to display if given. The sorting is done by directly sorting +the data in `UniqueXYZList`, which uses `ObservableList` to contain the data. It applies a comparator to +`UniqueXYZList` in `HireLah`, then applies the given predicate (if none, then use show all predicate) to `filteredXYZ` +filtered lists in `ModelManager`, which the `UI` will pick up and display the data to the user. -### \[Proposed\] Data archiving -_{Explain here how the data archiving feature will be implemented}_ +To support different sorting for different data types, each type of data sort is a comparator class in the Model component. +For example, for applicants, we will sort by their name, hence, there is a ApplicantNameComparator in the Model component +under applicant. The comparator implements Java's Comparator interface. +The *Sequence Diagram* below illustrates the interactions within the classes for the execution of `list -a s/asc` command. + + + +#### Design considerations: + +#### Aspect: How to sort data without affect the original dataset + +* **Alternative 1 (current choice):** Sort the `UniqueXYZList` and display the data using filtered lists predicate + * Pros: + * Less chance of error occurs when modify the displayed data. + * `UI` can displayed the sorted data immediately. + * `export -X` can export the data according to their sorting order. + * Cons: Decrease cohesion, as we need to depend on `HireLah`. + + +* **Alternative 2:** Directly sort the `filteredXYZ` filtered lists in `ModelManager` by passing it to sorted lists. + * Pros: Increase cohesion, as method only used attributes in `ModelManager`. + * Cons: + * Increased the complexity of the relevant code, as we need to double passing, which make it more bug-prone. + * `UI` won't able to display the new filtered lists, and need to connect again to `UI` components. + +### Exporting of Data + +#### Implementation + +Exporting of different data types is currently done through `ModelManger`, which implements the methods in interface `Model`. +There are 2 levels to the parsing of the add command from user input. +1. `HireLahParser` identifies it as an `export` command. +2. `ExportCsvCommandParser` identifies the exact data type that need to be exported, through the `flag` of the user input +, and returns the respective `ExportXYZCsvCommand`. + +The *Sequence Diagram* below illustrates the interactions within the classes for the execution of `export -p` command. + + + +#### Design considerations: + +#### Aspect: What export format should be used: + +* **Alternative 1 (current choice):** Export to CSV file + * Pros: + * Versatile since CSV file can be used by non-technical user. + * Suitable for manipulating `Applicant`, `Interview` and `Position` data. + * Cons: Requires additional method to transform `Model` into CSV output + +* **Alternative 2:** Export to individual Json file + * Pros: Able to reuse code as Json already implemented by `Storage` + * Cons: Not versatile as required non-user to have knowledge about Json. -------------------------------------------------------------------------------------------------------------------- ## **Documentation, logging, testing, configuration, dev-ops** - + * [Documentation guide](Documentation.md) * [Testing guide](Testing.md) * [Logging guide](Logging.md) @@ -257,42 +434,59 @@ _{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 +* Has a need to manage a significant number of applicants to technology companies +* Prefer desktop apps over other types +* Can type fast +* Prefers typing to mouse interactions +* Is reasonably comfortable using CLI apps +* Needs to view where each applicant is in the hiring pipeline -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +**Value proposition**: +* Manage applicants faster than a typical mouse/GUI driven app +* Schedule interviews for different applicants and match them to different positions +* End to end seamless administration for talent management ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | +| Priority | As a …​ | I want to …​ | So that I can…​ | +| ------ |-------------|----------------------------------------|------------------------------------------------------------------------| +| `* * *` | new user | see usage instructions of all commands | know what are the commands available and how to use them | +| `* * *` | recruiter | add a new applicant | keep track of all the applicants | +| `* * *` | recruiter | edit an applicant | update the latest information of applicants | +| `* * *` | recruiter | delete an applicant | remove applicants that have left the hiring pipeline | +| `* * *` | recruiter | add a new interview | potentially match an applicant to a job | +| `* * *` | recruiter | edit an interview | update the latest information of interviews | +| `* * *` | recruiter | delete an interview | remove interviews that are cancelled | +| `* * *` | recruiter | add a new position | keep track of all the job positions | +| `* * *` | recruiter | edit a position | update the latest information of positions | +| `* * *` | recruiter | delete a position | remove positions which are not available anymore | +| `* * *` | recruiter | view the applicants in my contact | access their information and contact them | +| `* * *` | recruiter | view the positions I am recruiting for | know what are the positions available | +| `* * *` | recruiter | view the interviews I have | know my schedule and plan my work day | +| `* *` | recruiter | filter the displayed data | find the information I am looking for easily | +| `* *` | recruiter | pass an interview that was successful | proceed to offer the applicant the position | +| `* *` | recruiter | fail an interview that was unsuccessful | proceed to end the hiring process for the applicant | +| `* *` | recruiter | mark an interview as accepted by the applicant | update that the applicant has accepted the offer | +| `* *` | recruiter | mark an interview as rejected by the applicant | update that the applicant has rejected the offer | +| `* *` | recruiter | export the data in the application | share the information with other recruiters | +| `*` | expert user | access previous commands I made | send multiple similar commands without having to type the whole command | -*{More to be added}* ### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is the `HireLah Application` and the **Actor** is the `user`, unless specified otherwise) -**Use case: Delete a person** +#### **Use case 01: Delete a applicant** **MSS** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User requests to list applicants +2. HireLah shows a list of applicants +3. User requests to delete a specific applicant in the list +4. HireLah deletes the applicant Use case ends. @@ -304,24 +498,279 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli * 3a. The given index is invalid. - * 3a1. AddressBook shows an error message. + * 3a1. HireLah shows an error message. Use case resumes at step 2. -*{More to be added}* +#### **Use case 02: Adding an applicant** -### Non-Functional Requirements +**MSS** + +1. User requests to add applicant with specific parameters. +2. HireLah shows list including new applicant. +

+ Use case ends. + +**Extensions** + +* 1a. HireLah detects specified parameters are incorrect. + * 1a1. HireLah shows an error message. +

+ Use case resumes at step 1. +

+ + +#### **Use case 03: Editing position** + +**MSS** +1. User requests to list positions +2. HireLah shows a list of positions +3. User chooses to edit a position based on the index from the visible list, and provide the fields to edit. +4. HireLah refreshes the list of positions to display the newly edited position. +

+ Use case ends. + +**Extensions** + +* 3a. The given index is not a valid index in the list. +* 3a1. HireLah informs user that the index is not valid. +

+ Use case ends. +

+* 3b. The new position name provided is the same as another position. +* 3b1. HireLah informs user that the new position name is not valid. +

+ Use case ends. + +#### **Use case 04: Viewing help** + +**MSS** +1. User requests to view help +2. HireLah shows a list of commands and its briefly description +

+Use case ends. + +#### **Use case 05: Viewing detail help for a specific command** + +**MSS** +1. User open the list of commands and general description (UC4). +2. User chooses a specific command and view its detail description. +3. HireLah displays the detail description of that command +

+Use case ends. + +**Extensions** + +* 2a. The given command is not a valid command. +* 2a1. HireLah informs user that HireLah don't have the command. +

+ Use case ends. +

+* 3b. The new position name provided is the same as another position. +* 3b1. HireLah informs user that the new position name is not valid. +

+ Use case ends. + +#### **Use case 06: Filtering data** + +**MSS** +1. User requests to list data with filter applied. +2. HireLah refreshes the list of data to display with only data that matches the filter given. +

+ Use case ends. + +**Extensions** -1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -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. +* 1a. No data in HireLah fits the filter given. +* 1a1. HireLah informs user that no data is found. +

+ Use case ends. +

+* 1b. The filter type given is invalid. +* 1b1. HireLah informs user that the filter type given is invalid. +

+ Use case ends. + +#### **Use case 07: Sorting data** -*{More to be added}* +**MSS** +1. User requests to list data with sorting applied. +2. HireLah refreshes the list of data to display with data that have been sorted. +

+ Use case ends. + +**Extensions** + +* 1a. No data of a given type in HireLah. +* 1a1. HireLah display an empty list. +

+ Use case ends. +

+* 1b. The sort argument given is invalid. +* 1b1. HireLah informs user that the sort argument given is invalid. +

+ Use case ends. + +#### **Use case 08: Passing interview** + +**MSS** +1. User requests to list interviews. +2. HireLah shows a list of interviews. +3. User choose to pass an interview based on the index from the visible list. +4. HireLah refreshes the list of interviews to display the newly passed interview. +

+ Use case ends. + +**Extensions** + +* 1a. No interview data in HireLah. +* 1a1. HireLah display an empty list. +

+ Use case ends. +

+* 3b. The given interview index is not a valid index in the list. +* 3b1. HireLah informs user that the index is invalid. +

+ Use case ends. +

+* 3c. The chosen interview is not at `Pending` status. +* 3c1. HireLah informs user that only pending interview can be passed +

+ Use case ends. +

+#### **Use case 09: Failing interview** + +**MSS** +1. User requests to list interviews. +2. HireLah shows a list of interviews. +3. User choose to fail an interview based on the index from the visible list. +4. HireLah refreshes the list of interviews to display the newly failed interview. +

+ Use case ends. + +**Extensions** + +* 1a. No interview data in HireLah. +* 1a1. HireLah display an empty list. +

+ Use case ends. +

+* 3b. The given interview index is not a valid index in the list. +* 3b1. HireLah informs user that the index is invalid. +

+ Use case ends. +

+* 3c. The chosen interview is not at `Pending` status. +* 3c1. HireLah informs user that only pending interview can be passed +

+ Use case ends. +

+#### **Use case 10: Accepting interview** + +**MSS** +1. User requests to list interviews. +2. HireLah shows a list of interviews. +3. User choose to accept an interview based on the index from the visible list. +4. HireLah refreshes the list of interviews to display the newly accepted interview. +

+ Use case ends. + +**Extensions** + +* 1a. No interview data in HireLah. +* 1a1. HireLah display an empty list. +

+ Use case ends. +

+* 3b. The given interview index is not a valid index in the list. +* 3b1. HireLah informs user that the index is invalid. +

+ Use case ends. +

+* 3c. The chosen interview is not at `Pass` status. +* 3c1. HireLah informs user that only passed interview can be passed +

+ Use case ends. +

+ +#### **Use case 11: Rejecting interview** + +**MSS** +1. User requests to list interviews. +2. HireLah shows a list of interviews. +3. User choose to reject an interview based on the index from the visible list. +4. HireLah refreshes the list of interviews to display the newly rejected interview. +

+ Use case ends. + +**Extensions** + +* 1a. No interview data in HireLah. +* 1a1. HireLah display an empty list. +

+ Use case ends. +

+* 3b. The given interview index is not a valid index in the list. +* 3b1. HireLah informs user that the index is invalid. +

+ Use case ends. +

+* 3c. The chosen interview is not at `Pass` status. +* 3c1. HireLah informs user that only passed interview can be passed +

+ Use case ends. +

+ +#### **Use case 12: Exporting data** + +**MSS** +1. User requests to list data with optional sort or filter argument. +2. HireLah shows a list of data. +3. User chooses to export the data. +4. HireLah exports the current displayed data into a CSV file. +

+ Use case ends. + +**Extensions** + +* 1a. No data in HireLah. +* 1a1. HireLah display an empty list. +

+ Use case ends. +

+* 4b. User chooses to open the CSV file and open again. +* 4b1. HireLah informs user that the user need to close the CSV before exporting. +

+ Use case ends. +

+ +#### **Use case 13: Clearing data** + +**MSS** +1. User requests to clear data. +2. HireLah clear all data. +

+ Use case ends. + +### Non-Functional Requirements + +1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. +2. HireLah should respond within two seconds after any command is entered. +3. Should be able to hold up to 1000 applicants, positions, and interviews each without a noticeable sluggishness in performance for typical usage. +4. The data in the app should be easily transferable to another computer without losing any information. +5. 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. +6. The system should be usable by a novice which has not used other CLI application for recruitment tracking. +7. A new user should be able to pick up how to use HireLah within 20 minutes of usage. +8. HireLah must boot up within 10 seconds on a device under a normal load. +9. HireLah is not required to make any direct communication with the applicants. ### Glossary * **Mainstream OS**: Windows, Linux, Unix, OS-X -* **Private contact detail**: A contact detail that is not meant to be shared with others +* **Recruiter**: A Human Resource professional that manages applicants, interviews and positions in the application +* **Applicant**: A candidate looking for a job. +* **Interview**: A scheduled meeting time for an Applicant to try for a Position. +* **Position**: A job opportunity for candidates. -------------------------------------------------------------------------------------------------------------------- @@ -340,7 +789,7 @@ testers are expected to do more *exploratory* testing. 1. Download the jar file and copy into an empty folder - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 1. Double-click the jar file Expected: Shows the GUI with a set of sample data. The window size may not be optimum. 1. Saving window preferences @@ -348,30 +797,182 @@ testers are expected to do more *exploratory* testing. 1. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. + +### Adding Data +1. Adding an applicant to HireLah + 1. Prerequisites: Ensure that cursor is on command box. Current applicant list does not contain any other applicants with the same name, phone number, or email, stated in Test Case #1. + 2. Test case: `add -a n/Jonathan p/98564231 e/jonathan@example.com ag/23 a/73 Geylang Rd, S532948 g/M t/NUS Graduate`
+ Expected: New applicant is added to the bottom of the list. Details of the applicant shown in response box. GUI toggles to display applicant list. + 3. Test case: `add -a n/Jonathan p/91234567 e/notjonathan@example.com ag/23 a/73 Geylang Rd, S532948 g/M t/NUS Graduate`
+ Expected: No applicant is added to the applicant list. Error message informs user that the applicant already exists. + 4. Test case: `add -a n/NotJonathan p/98564231 e/notjonathan@example.com ag/23 a/73 Geylang Rd, S532948 g/M t/NUS Graduate`
+ Expected: No applicant is added to the applicant list. Error message informs user that phone number is in used by "Jonathan". + 5. Test case: `add -a n/NotJonathan p/91234567 e/jonathan@example.com ag/23 a/73 Geylang Rd, S532948 g/M t/NUS Graduate`
+ Expected: No applicant is added to the applicant list. Error message informs user that email is in used by "Jonathan". +1. Adding a position to HireLah + 1. Prerequisites: Ensure that cursor is on command box. Current position list does not contain any other positions with the same position name stated in Test Case #1. + 2. Test case: `add -p p/Junior Software Developer o/3 d/One of the highest compensation in the market. Work is remote. r/Golang r/Cloud Computing`
+ Expected: New position is added to the bottom of the list. Details of the position shown in response box. GUI toggles to display position list. + 3. Test case: `add -p p/Junior Software Developer o/5 d/Not the same description. r/C++ r/Java`
+ Expected: No position is added to the position list. Error message informs user that position already exists. +1. Adding an interview to HireLah + 1. Prerequisites: Ensure that cursor is on command box. At least one applicant and position in the application. No interview scheduled for applicant and the particular position mentioned in all test cases below. + 2. Test case: `add -i 1 p/1 d/2022-04-11 12:00`
+ Expected: New interview is added to the bottom of the list. Applicant's name, date of interview, position's name and interview status is shown in response. GUI toggles to display interview list. + 3. Test case: `add -i 1 p/1 d/2022-04-12 12:00`
+ Expected: No interview is added to the interview list. Error message informs user that applicant already has an interview scheduled for that position. + 4. Test case: `add -i 1 p/2 d/2022-04-11 12:30`
+ Expected: No interview is added to the interview list. Error message informs user that applicant has an existing scheduled interview that clashes in timing. + +### Deleting Data + +1. Deleting an applicant while all applicants are being shown + + 1. Prerequisites: List all applicants using the `list -a` command. Multiple applicants in the list. + + 1. Test case: `delete -a 1`
+ Expected: First applicant is deleted from the list. Details of the deleted applicant shown together with the number of deleted interview(s). Use `list -i` to verify that the interview(s) involving the deleted applicant no longer exists. + + 1. Test case: `delete -a 0`
+ Expected: No applicant is deleted. Error details shown. + + 1. Other incorrect delete applicant commands to try: `delete -a`, `delete -a x` (where x is larger than the list size)
+ Expected: Similar to previous. + +2. Deleting an applicant while the applicant list is filtered + + 1. Prerequisites: Filter the applicants using the `list -a f/name a/xxx` command (where xxx is an existing applicant name). At least one applicant in the list. + + 1. Test case: `delete -a 1`
+ Expected: First applicant is deleted from the list. Details of the deleted applicant shown together with the number of deleted interview(s). Use `list -i` to verify that the interview(s) involving the deleted position no longer exists. -1. _{ more test cases …​ }_ + 1. Test case: `delete -a 0`
+ Expected: No applicant is deleted. Error details shown. -### Deleting a person +3. Deleting a position + 1. Prerequisites: List positions using the `list -p` command, may choose to apply a valid filter. Multiple positions in the list. -1. Deleting a person while all persons are being shown + 2. Test case: `delete -p 2`
+ Expected: Second position is deleted from the list. Details of the deleted position shown together with the number of deleted interview(s). - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + 3. Test case: `delete -p 0`
+ Expected: No position is deleted. Error details shown. - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. + 4. Other incorrect delete position commands to try: `delete -p`, `delete -p x` (where x is larger than the list size)
+ Expected: Similar to previous. + +4. Deleting an interview + 1. Prerequisites: List interviews using the `list -i` command, may choose to apply a valid filter. Multiple interviews in the list. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + 2. Test case: `delete -i 1`
+ Expected: First interview is deleted from the list. Details of the deleted interview shown. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ 3. Test case: `delete -i 0`
+ Expected: No interview is deleted. Error details shown. + + 4. Other incorrect delete position commands to try: `delete -i`, `delete -i x` (where x is larger than the list size)
+ Expected: Similar to previous. + +5. Delete without any flag specified + 1. Test case: `delete`
+ Expected: No data is deleted. "No flag" error shown. + + 2. Test case: `delete 2`
+ Expected: No data is deleted. "No flag" error shown. + +### Filtering Data + +1. Filtering applicants + 1. Prerequisites: List all applicants using the `list -a` command. Multiple applicants in the list. + + 2. Test case: `list -a f/name a/xxx` where xxx is a valid name of an applicant in the list.
+ Expected: The list refreshes showing only applicants whose name matches the name given. Shows message of how many applicants are listed. + + 3. Test case: `list -a f/name a/xxx` where xxx is not a name of any applicant in the list.
+ Expected: An empty applicant list is shown. + + 4. Test case: `list -a f/name`
+ Expected: The applicant list does not refresh. Error message shown. + + 5. Other incorrect filter applicant commands to try: `list -a f/abc a/abc`, `list -a a/John`
Expected: Similar to previous. -1. _{ more test cases …​ }_ +2. Filtering positions + 1. Prerequisites: List all positions using the `list -p` command. Multiple positions in the list. + + 2. Test case: `list -p f/name a/xxx` where xxx is a valid name of a position in the list.
+ Expected: The list refreshes showing only positions with name that matches the name given. Shows message of how many positions are listed. + + 3. Test case: `list -p f/name a/xxx` where xxx is not a name of any position in the list.
+ Expected: An empty position list is shown. + + 4. Test case: `list -p f/name`
+ Expected: The position list does not refresh. Error message shown. + + 5. Other incorrect filter applicant commands to try: `list -p f/abc a/abc`, `list -p a/Software`
+ Expected: Similar to previous. + +3. Filtering interviews + 1. Prerequisites: List all interviews using the `list -i` command. Multiple interviews in the list. + + 2. Test case: `list -i f/date a/yyyy-mm-dd` where yyyy-mm-dd is a valid date of an interview in the list.
+ Expected: The list refreshes showing only interviews with date that falls the date given. Shows message of how many interviews are listed. + + 3. Test case: `list -i f/date a/yyyy-mm-dd` where yyyy-mm-dd is a date of any interview in the list.
+ Expected: An empty interview list is shown. + + 4. Test case: `list -i f/date`
+ Expected: The position list does not refresh. Error message shown. + + 5. Other incorrect filter interview commands to try: `list -i f/abc a/abc`, `list -i a/2022-05-05`
+ Expected: Similar to previous. +### Sorting Data +1. Sorting applicants + 1. Prerequisites: List all applicants using the `list -a` command. At least two applicants in the list. + + 2. Test case: `list -a s/asc`
+ Expected: The list refreshes showing the list of all applicants sorted by their names in ascending order (if not already). + + 3. Test case: `list -a s/abc`
+ Expected: An error message is shown. + +2. Sorting positions + 1. Prerequisites: List all positions using the `list -p` command. At least two positions in the list. + + 2. Test case: `list -p s/asc`
+ Expected: The list refreshes showing the list of all positions sorted by their names in ascending order (if not already). + + 3. Test case: `list -p s/abc`
+ Expected: An error message is shown. + +2. Sorting interviews + 1. Prerequisites: List all interviews using the `list -i` command. At least two interviews in the list. + + 2. Test case: `list -i s/asc`
+ Expected: The list refreshes showing the list of all interviews sorted by their date in ascending order (if not already). + + 3. Test case: `list -i s/abc`
+ Expected: An error message is shown. + + ### Saving data -1. Dealing with missing/corrupted data files +1. Saving newly added/edited data + + 1. Add/edit any data in the app (applicant / position / interview). + 2. Restart the app by exiting and opening the jar file.
+ Expected: Any changes made to the data is retained. + +2. Dealing with missing data file - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. Exit the app and delete the storage file at `/data/HireLah.json`. + 2. Re-launch the app by opening the jar file.
+ Expected: The app launches with sample data. + +2. Dealing with corrupted data file -1. _{ more test cases …​ }_ + 1. Exit the app and open the storage file at `/data/HireLah.json`. + 2. Remove a comma `,` from the file. + 3. Re-launch the app by opening the jar file.
+ Expected: The app launches with no data. Gives warning in log. diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 275445bd551..b09c206855c 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -45,7 +45,7 @@ If you plan to use Intellij IDEA (highly recommended): 1. **Learn the design** - When you are ready to start coding, we recommend that you get some sense of the overall design by reading about [AddressBook’s architecture](DeveloperGuide.md#architecture). + When you are ready to start coding, we recommend that you get some sense of the overall design by reading about [HireLah’s architecture](DeveloperGuide.md#architecture). 1. **Do the tutorials** These tutorials will help you get acquainted with the codebase. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 3716f3ca8a4..d52cf99c673 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,7 +3,9 @@ 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. +HireLah is a desktop app that helps **recruiters to manage talent and job candidates** by tracking every step of the hiring process, +from offering positions to scheduling interviews with candidates. It is optimised for Command Line Interface (CLI) users while still offering a GUI, so that power users can accomplish tasks much quicker by using commands + * Table of Contents {:toc} @@ -14,27 +16,29 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo 1. Ensure you have Java `11` or above installed in your Computer. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Download the latest `HireLah.jar` from [here](https://github.com/AY2122S2-CS2103-W17-4/tp/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +3. Copy the file to the folder you want to use as the _home folder_ for your HireLah. -1. Double-click the file to start the app. The GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
+4. Double-click the file to start the app. The 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. e.g. typing **`help`** and pressing Enter will open the help window.
Some example commands you can try: - * **`list`** : Lists all contacts. - - * **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. + * `list -i`: Switches to interview tab and displays all interviews. - * **`delete`**`3` : Deletes the 3rd contact shown in the current list. + * `list -a s/asc`: Switches to applicants tab and displays all applicants sorted by name in ascending order. - * **`clear`** : Deletes all contacts. + * `add -a n/Benedict ag/20 g/M p/98123456 e/ben@gmail.com a/12 Kent Ridge Drive, 119243`: Adds an applicant named `Benedict` to the HireLah. - * **`exit`** : Exits the app. + * `delete -i 2`: Deletes the 2nd interview shown in the current interview list. + + * `export -a`: Exports the displayed data of all applicants in HireLah to a CSV file. + + * `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. -------------------------------------------------------------------------------------------------------------------- @@ -45,13 +49,13 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo **:information_source: Notes about the command format:**
* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. + e.g. in `add -a n/NAME`, `NAME` is a parameter which can be used as `add n/Benedict`. * Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. + e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/C++` or as `n/John Doe`. * Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. + e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/C++`, `t/Java t/C++` etc. * Parameters can be in any order.
e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. @@ -59,134 +63,416 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo * If a parameter is expected only once in the command but you specified it multiple times, only the last occurrence of the parameter will be taken.
e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` will be taken. -* 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`. +* Extraneous parameters for commands that do not take in parameters (such as `exit`) will be ignored.
+ e.g. if the command specifies `exit 123`, it will be interpreted as `exit`. + +* The largest value for a positive integer is 2147483647, which is the maximum value usable for all the index. -### Viewing help : `help` -Shows a message explaning how to access the help page. +## Add +General command to add different data types into HireLah. -![help message](images/helpMessage.png) +Format: `add -TYPE …​` +* TYPE must take the form of `a`, `i`, `p` + * `-a` to indicate adding an applicant + * `-i` to indicate adding an interview + * `-p` to indicate adding a position -Format: `help` +### Adding Applicant: `add -a` +Adds a new applicant to HireLah + +Format: `add -a n/APPLICANT_NAME ag/AGE g/GENDER p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` + +
:bulb: **Tip:** +An applicant can have any number of tags (including 0) +
+ +* Age provided must be **exactly two digits** and should not start with 0 eg: “23”. +* Two applicants cannot have the same Name, Phone Number or Email. +* Gender **must be M/F**. (case-sensitive) + +Examples: +* `add -a n/Benedict ag/20 g/M p/98123456 e/ben@gmail.com a/12 Kent Ridge Drive, 119243` +* `add -a n/Max ag/15 g/M p/97123456 e/max@yahoo.com a/12 Kent Ridge Drive, 119243 t/Data Analyst` + +### Adding Interview : `add -i` + +Adds a new interview to HireLah. + +Format: `add -i APPLICANT_INDEX d/DATE p/POSITION_INDEX` +* Date provided must be in format YYYY-MM-DD HH:MM. +* All interviews added have a **duration of 1 hour**. +* An applicant can only have interviews if they are **at least 1 hour (60 minutes) apart**. For example, + an applicant can have an interview at `2022-01-01 13:00` and again at `2022-01-01 14:00`, + but not at `2022-01-01 13:50`. +* An applicant can only have **at most one interview for each unique position** in the position list. +* An interview cannot be created for a Position that has no openings. +* An interview cannot be created for an Applicant that has already been hired. +* The `APPLICANT_INDEX` refers to the index number shown in the last displayed Applicant + list. +* The `POSITION_INDEX` refers to the index number shown in the last displayed Position + list. +* Index provided **must be a positive integer** 1, 2, 3, …​ + +Examples: +* `add -i 1 d/2022-01-01 14:00 p/2` -### Adding a person: `add` +### Adding Positions : `add -p` -Adds a person to the address book. +Adds a new open position to HireLah. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +Format: `add -p p/POSITION_NAME o/NUM_OPENINGS d/DESCRIPTION [r/REQUIREMENTS]`
:bulb: **Tip:** -A person can have any number of tags (including 0) +A position can have any number of requirements (including 0)
+* Positions must have a **unique name**. +* Name provided is case-insensitive. +* Number of openings in the position must be between **1 and 5 digits**. +* Description must be between **1 and 200 characters**. +* Name must be between **1 and 100 characters**. + Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* `add -p p/Senior Software Engineer o/3 d/More than 5 years experience r/JavaScript r/HTML r/CSS` -### Listing all persons : `list` -Shows a list of all persons in the address book. +## Edit +General command to edit different types into HireLah. -Format: `list` +Format: `edit -TYPE …​` +* TYPE must take the form of `a`, `i`, `p` + * `-a` to indicate editing an applicant + * `-i` to indicate editing an interview + * `-p` to indicate editing a position -### Editing a person : `edit` +### Editing Applicant : `edit -a` -Edits an existing person in the address book. +Edits an existing Applicant in HireLah. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Format: `edit -a APPLICANT_INDEX [n/APPLICANT_NAME] [ag/AGE] [g/GENDER] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ -* At least one of the optional fields must be provided. +* Edits the Applicant at the specified `APPLICANT_INDEX`. +* The `APPLICANT_INDEX` refers to the index number shown in the last displayed applicant list. +* The `APPLICANT_INDEX` **must be a positive integer** 1, 2, 3, …​ +* **At least one** of the optional fields must be provided. * Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +* When editing tags, the existing tags of the applicant will be removed i.e adding of tags is not cumulative. +* You can remove all the Applicant’s tags by typing `t/` without + specifying any tags after it. + +Examples: +* `edit -a 1 n/Belle ag/43 g/F p/81234567` +* `edit -a 2 e/cedric@yahoo.com a/13 Computing Drive 612345 t/` + +### Editing Interview : `edit -i` + +Edits an existing interview in HireLah. + +Format: `edit -i INTERVIEW_INDEX [a/APPLICANT_INDEX] [d/DATE] [p/POSITION_INDEX]` +* Date provided must be in format YYYY-MM-DD HH:MM. +* Edits the interview at the specified `INTERVIEW_INDEX`. +* Only interviews with `Pending` status can be edited. +* The `INTERVIEW_INDEX` refers to the index number shown in the last displayed interview list. +* The `APPLICANT_INDEX` refers to the index number shown in the last displayed applicant list. +* The `POSITION_INDEX` refers to the index number shown in the last displayed position list. +* **At least one** optional field must be provided. +* Existing attribute of the interview will be updated to the input value. + +Examples: +* `edit -i 1 d/2022-01-01 15:00` +* `edit -i 3 a/1 d/2022-01-01 15:00 p/1` + + +### Editing Positions : `edit -p` + +Edits an existing position in HireLah. + +Format: `edit -p POSITION_INDEX [p/POSITION_NAME] [o/NUM_OPENINGS] [d/DESCRIPTION] [r/REQUIREMENTS]` + +* Edits the available position at the specified `POSITION_INDEX`. +* The `POSITION_INDEX` refers to the index number shown in the last displayed position list. +* At least one optional field must be provided. +* Existing attributes of the position will be updated to the input value. +* When editing requirements, the existing requirements of the position will be removed. i.e. adding requirements is not cumulative. +* Requirements can be removed by providing an empty requirement field. i.e. r/ +* Number of openings in the position must be between **1 and 5 digits**. +* Number of openings in the position cannot be edited to be lower than the current number of outstanding offers. +* Description must be between **1 and 200 characters**. +* Name must be between **1 and 100 characters**. Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +* `edit -p 1 p/Senior Frontend Software Engineer o/5` +* `edit -p 3 p/Senior Frontend Software Engineer r/JavaScript r/React` + -### Locating persons by name: `find` +## Delete +General command to delete different data type in HireLah. -Finds persons whose names contain any of the given keywords. +Format: `delete -TYPE …​` +* TYPE must take the form of `a`, `i`, `p` + * `-a` to indicate deleting an applicant + * `-i` to indicate deleting an interview + * `-p` to indicate deleting a position -Format: `find KEYWORD [MORE_KEYWORDS]` +### Deleting Applicant : `delete -a` -* 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` +Deletes the specified Applicant from HireLah. + +Format: `delete -a APPLICANT_INDEX` + +* Deletes the Applicant at the specified `APPLICANT_INDEX`. +* Interviews that contain said applicant are also deleted. +* Offers for Positions handed out to said applicant will also be removed. +* The index refers to the index number shown in the displayed Applicant list. +* The index **must be a positive integer** 1, 2, 3, …​ Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +* `delete -a 1` + +### Deleting Interview: `delete -i` + +Deletes an existing interview in HireLah. + +Format: `delete -i INTERVIEW_INDEX` +* Deletes the Interview at the specified `INTERVIEW_INDEX`. +* Offer for Positions handed out via the interview will also be removed. +* The index refers to the index number shown in the displayed Interview list. +* The index **must be a positive integer** 1, 2, 3, …​ + +Examples: +* `delete -i 2` + +### Deleting Positions : `delete -p` + +Deletes an existing position in HireLah. + +Format: `delete -p POSITION_INDEX` +* Deletes the Position at the specified `POSITION_INDEX`. +* Interviews that contain said position are also deleted. +* However, Applicants that have already accepted a job at said Position, will retain their status as being hired for that Position. +* The index refers to the index number shown in the displayed Position list. +* The index **must be a positive integer** 1, 2, 3, …​ + +Examples: +* `delete -p 3` + +## List +General command to list different data types in HireLah. Users can provide optional parameters to filter and sort the data to display. +Users can either display all data, with filter only, with sort only, or with both filter and sort. + +Note: This command may change the index of the displayed items, and all other commands that accepts an index will follow the latest index shown. + +Format: `list -TYPE [f/FILTER_TYPE a/FILTER_ARGUMENT] [s/SORT_ARGUMENT]` +* TYPE must take the form of `a`, `i`, `p` + * `-a` to indicate deleting an applicant + * `-i` to indicate deleting an interview + * `-p` to indicate deleting a position + +* If there are no parameters provided, HireLah will display all data of that type by default. +* `FILTER_TYPE` and `FILTER_ARGUMENT` are optional parameters to filter the data displayed. + * Note that **both** `FILTER_TYPE` and `FILTER_ARGUMENT` need to be provided to filter data. + * Different data types will accept different `FILTER_TYPE` and `FILTER_ARGUMENT`, as elaborated in the table below. + +* `SORT_ARGUMENT` is the optional parameter to sort the data displayed. + * Can either be `asc` or `dsc`. + * Sorting only works based on a predetermined attribute, and different data types will be sorted according to different attributes, as elaborated in the table below. + +### Listing Applicants: `list -a [f/FILTER_TYPE a/FILTER_ARGUMENT] [s/SORT_ARGUMENT]` +Lists all applicants by default. Automatically toggles view to the applicant tab on the GUI. + +The applicants displayed can be filtered by providing the optional parameters `f/FILTER_TYPE` and `a/FILTER_ARGUMENT`: + + +| FILTER_TYPE | FILTER_ARGUMENT | Description | +|-------------|------------------------------------|----------------------------------------------------------| +| `name` | Keyword(s) in the applicant's name | View applicants whose name contains the keyword(s) | +| `gender` | M / F (Case-sensitive) | View applicants of the given gender | +| `status` | available / hired | View applicants with the status given | +| `tag` | Keyword(s) in the applicant's tag | View applicants with a tag that matches the keywords(s) | -### Deleting a person : `delete` +The applicants displayed can be sorted by their **name** using the parameter `s/SORT_ARGUMENT`. -Deletes the specified person from the address book. +Examples: +- `list -a s/asc` +- `list -a f/name a/John Doe` +- `list -a f/tag a/Java` +- `list -a f/status a/hired s/dsc` -Format: `delete INDEX` +### Listing interviews: `list -i [f/FILTER_TYPE a/FILTER_ARGUMENT] [s/SORT_ARGUMENT]` -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. +Lists all existing interviews by default. Automatically toggles view to the interview tab on the GUI. + +The interviews displayed can be filtered by providing the optional parameters `f/FILTER_TYPE` and `a/FILTER_ARGUMENT`: + +| FILTER_TYPE | FILTER_ARGUMENT | Description | +|-------------|----------------------------------------------------------|----------------------------------------------------------------------| +| `appl` | Keyword(s) in the applicant's name | View interviews for applicants whose name contains the keyword(s) | +| `pos` | Keyword(s) in the position's name | View interviews for position with names that contains the keyword(s) | +| `date` | Date the interview is happening in `yyyy-mm-dd` | View interviews which happens on the date provided | +| `status` | pending / passed / failed / accepted / rejected | View interviews with the status given | + +The interviews displayed can be sorted by their **date** using the parameter `s/SORT_ARGUMENT`. + +Examples: +- `list -i s/dsc` +- `list -i f/date a/2022-05-04` +- `list -i f/status a/accepted s/asc` + +### Listing Positions : `list -p [f/FILTER_TYPE a/FILTER_ARGUMENT] [s/SORT_ARGUMENT]` + +Lists all existing positions by default. Automatically toggles view to the position tab on the GUI. + +The positions displayed can be filtered by providing the optional parameters `f/FILTER_TYPE` and `a/FILTER_ARGUMENT`: + +| FILTER_TYPE | FILTER_ARGUMENT | Description | +|-------------|---------------------------------|-----------------------------------------------------------------| +| `name` | Keyword(s) in the position name | View positions with names that contains the keyword(s) | +| `req` | Keyword(s) in the requirement | View positions with a requirement that contains the keywords(s) | + +The positions displayed can be sorted by their **name** using the parameter `s/SORT_ARGUMENT`. + +Examples: +- `list -p s/asc` +- `list -p f/name a/Software Engineer` +- `list -p f/req a/Java s/dsc` + +## Passing Interviews : `pass` + +Passes an existing interview in HireLah. + +Format: `pass INTERVIEW_INDEX` + +* Passes the Interview at the specified `INTERVIEW_INDEX`. +* The `INTERVIEW_INDEX` refers to the index number shown in the last displayed interview list. +* Interview must have status `Pending` before it can be passed. +* The Applicant in the Interview must not be hired already, else the Interview cannot be passed. +* The index **must be a positive integer** 1, 2, 3, …​ + +Additional details: +* A job offer is handed out for the interviewed position when the applicant passes an interview. +* Job offer is tracked by the `Position` interviewed for. +* Job can only be offered if `offered` is less than `openings`. +* A job offered will increase `offered` by 1. + +Example: `pass 1` + +## Failing Interviews : `fail` + +Fails an existing interview in HireLah. + +Format: `fail INTERVIEW_INDEX` + +* Fails the Interview at the specified `INTERVIEW_INDEX`. +* The `INTERVIEW_INDEX` refers to the index number shown in the last displayed interview list. +* Interview must have status `Pending` before it can be failed. +* The index **must be a positive integer** 1, 2, 3, …​ + +Example: `fail 1` + +## Accepting Interviews : `accept` + +Accepts an existing `passed` interview in HireLah. This command accepts the `passed` interview, +meaning that the candidate has accepted the job. + +Format: `accept INTERVIEW_INDEX` + +* Accepts the Interview at the specified `INTERVIEW_INDEX`. +* The `INTERVIEW_INDEX` refers to the index number shown in the last displayed interview list. +* Interview must have status `Passed` before it can be accepted. +* The Applicant in the Interview must not be hired already, else the Interview cannot be accepted. * The index **must be a positive integer** 1, 2, 3, …​ +Additional details: +* Accepting a job offer will decrement number of `openings` and `offers`, as a vacancy in the `Position` is now filled. +* `Applicant` status will be updated to reflect the new job title. +* Note that editing a `Position`'s name after an `Applicant` has been hired will not change the `Applicant`'s job status. + +Example: `accept 1` + +## Rejecting Interviews : `reject` + +Rejects an existing interview in HireLah. This command rejects the `passed` interview, +meaning that the candidate has rejected the job. + +Format: `reject INTERVIEW_INDEX` + +* Rejects the Interview at the specified `INTERVIEW_INDEX`. +* The `INTERVIEW_INDEX` refers to the index number shown in the last displayed interview list. +* Interview must have status `Passed` before it can be rejected. +* The index **must be a positive integer** 1, 2, 3, …​ + +Additional details: +* Rejecting a job offer will decrement the number of `offered` in `Position`, as the offer no longer stands. + +Example: `reject 1` + +## Exporting Data : `export` + +Exports all current displayed data of the specified data type in HireLah to a CSV file. +The export csv file will be stored at export_csv folder. +* The export csv file only contain data that are currently displayed in HireLah +Format: `export -TYPE` +* TYPE must take the form of `a`, `i`, `p` + * `-a` to indicate exporting applicants + * `-i` to indicate exporting interviews + * `-p` to indicate exporting positions + Examples: -* `list` followed by `delete 2` deletes the 2nd person in the address book. -* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. +* `export -p` will export all positions to the corresponding csv file. +* `list -a f/name a/Betsy` then `export -a` will export csv all applicants named Betsy to the corresponding csv file. -### Clearing all entries : `clear` +## Clearing Data : `clear` -Clears all entries from the address book. +Clears all data in HireLah, including all `applicants`, `positions`, and `interviews`. Format: `clear` -### Exiting the program : `exit` +Warning: This command cannot be undone. -Exits the program. +## Viewing Help : `help` -Format: `exit` +Lists all the command keywords with their general descriptions + +Format: `help` -### Saving the data +For a more detail description about a specific `COMMAND`, you can type in the following: -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +Format: `help COMMAND` -### Editing the data file +* Full description and format of the command will be displayed +* Command name is not case-sensitive -AddressBook data are saved as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +Examples: +* `help add` +* `help delete` -
: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. -
+## Exiting the program : `exit` -### Archiving data files `[coming in v2.0]` +Exits the program. + +Format: `exit` -_Details coming soon ..._ +## Saving the data + +Upon exiting HireLah, the data in the application will automatically be saved, including the positions, applicants, and interviews. There is no need to save manually. +You should not modify the JSON file to preserve the integrity of the data. -------------------------------------------------------------------------------------------------------------------- ## FAQ **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous HireLah home folder. --------------------------------------------------------------------------------------------------------------------- +**Q**: How do I schedule an interview for a new applicant?
+**A**: You will need to first create a new applicant in HireLah, and ensure that the applied position exists in the system, else you will need to create the position as well. To schedule an interview, simply create a new interview with the applicant and the position. + +**Q**: Can I add an applicant without any interviews scheduled?
+**A**: Yes, you can simply add a new applicant in HireLah without adding any interviews. -## Command summary - -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +**Q**: I have a position that is only open for one applicant, do I still have to add the position?
+**A**: Yes, you will need to add the position as well even if it is only used once. diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..a1008119afb 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "HireLah" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2122S2-CS2103-W17-4/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..6a21a788bce 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: "HireLah"; font-size: 32px; } } diff --git a/docs/diagrams/AddParser.puml b/docs/diagrams/AddParser.puml new file mode 100644 index 00000000000..0d5bfe2ee06 --- /dev/null +++ b/docs/diagrams/AddParser.puml @@ -0,0 +1,59 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":HireLahParser" as HireLahParser LOGIC_COLOR +participant ":AddCommandParser" as AddCommandParser LOGIC_COLOR +participant ":AddInterviewCommandParser" as AddInterviewCommandParser LOGIC_COLOR +participant "a:AddInterviewCommand" as AddInterviewCommand LOGIC_COLOR +end box + +[-> HireLahParser : parseCommand("add -i 1 d/2022-01-01 14:00 p/2") +activate HireLahParser + +create AddCommandParser +HireLahParser -> AddCommandParser +activate AddCommandParser + +AddCommandParser --> HireLahParser +deactivate AddCommandParser + +HireLahParser -> AddCommandParser : parse("-i 1 d/2022-01-01 14:00 p/2") +activate AddCommandParser + +create AddInterviewCommandParser +AddCommandParser -> AddInterviewCommandParser +activate AddInterviewCommandParser + +AddInterviewCommandParser --> AddCommandParser +deactivate AddInterviewCommandParser + +AddCommandParser -> AddInterviewCommandParser : parse("1 d/2022-01-01 14:00 p/2") +activate AddInterviewCommandParser + +create AddInterviewCommand +AddInterviewCommandParser -> AddInterviewCommand +activate AddInterviewCommand + +AddInterviewCommand -> AddInterviewCommandParser : a +deactivate AddInterviewCommand + +AddInterviewCommandParser --> AddCommandParser : a +deactivate AddInterviewCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddInterviewCommandParser -[hidden]-> AddCommandParser +destroy AddInterviewCommandParser + +AddCommandParser --> HireLahParser : a +deactivate AddCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddCommandParser -[hidden]-> HireLahParser +destroy AddCommandParser + +[<--HireLahParser : a +deactivate HireLahParser + + +@enduml diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index ef81d18c337..afc82153abf 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -7,19 +7,19 @@ Participant ":Logic" as logic LOGIC_COLOR Participant ":Model" as model MODEL_COLOR Participant ":Storage" as storage STORAGE_COLOR -user -[USER_COLOR]> ui : "delete 1" +user -[USER_COLOR]> ui : "delete -a 1" activate ui UI_COLOR -ui -[UI_COLOR]> logic : execute("delete 1") +ui -[UI_COLOR]> logic : execute("delete -a 1") activate logic LOGIC_COLOR -logic -[LOGIC_COLOR]> model : deletePerson(p) +logic -[LOGIC_COLOR]> model : deleteApplicant(target) activate model MODEL_COLOR model -[MODEL_COLOR]-> logic deactivate model -logic -[LOGIC_COLOR]> storage : saveAddressBook(addressBook) +logic -[LOGIC_COLOR]> storage : saveHireLah(hireLah) activate storage STORAGE_COLOR storage -[STORAGE_COLOR]> storage : Save to file diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml deleted file mode 100644 index 5731f9cbaa1..00000000000 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ /dev/null @@ -1,21 +0,0 @@ -@startuml -!include style.puml -skinparam arrowThickness 1.1 -skinparam arrowColor MODEL_COLOR -skinparam classBackgroundColor MODEL_COLOR - -AddressBook *-right-> "1" UniquePersonList -AddressBook *-right-> "1" UniqueTagList -UniqueTagList -[hidden]down- UniquePersonList -UniqueTagList -[hidden]down- UniquePersonList - -UniqueTagList *-right-> "*" Tag -UniquePersonList -right-> Person - -Person -up-> "*" Tag - -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -@enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 1dc2311b245..aae768ef031 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -3,9 +3,10 @@ box Logic LOGIC_COLOR_T1 participant ":LogicManager" as LogicManager LOGIC_COLOR -participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":HireLahParser" as HireLahParser LOGIC_COLOR participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR -participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR +participant ":DeleteApplicantCommandParser" as DeleteApplicantCommandParser LOGIC_COLOR +participant "d:DeleteApplicantCommand" as DeleteApplicantCommand LOGIC_COLOR participant ":CommandResult" as CommandResult LOGIC_COLOR end box @@ -13,56 +14,94 @@ box Model MODEL_COLOR_T1 participant ":Model" as Model MODEL_COLOR end box -[-> LogicManager : execute("delete 1") +[-> LogicManager : execute("delete -a 1") activate LogicManager -LogicManager -> AddressBookParser : parseCommand("delete 1") -activate AddressBookParser +LogicManager -> HireLahParser : parseCommand("delete -a 1") +activate HireLahParser create DeleteCommandParser -AddressBookParser -> DeleteCommandParser +HireLahParser -> DeleteCommandParser activate DeleteCommandParser -DeleteCommandParser --> AddressBookParser +DeleteCommandParser --> HireLahParser deactivate DeleteCommandParser -AddressBookParser -> DeleteCommandParser : parse("1") +HireLahParser -> DeleteCommandParser : parse("-a 1") activate DeleteCommandParser -create DeleteCommand -DeleteCommandParser -> DeleteCommand -activate DeleteCommand +create DeleteApplicantCommandParser +DeleteCommandParser -> DeleteApplicantCommandParser +activate DeleteApplicantCommandParser -DeleteCommand --> DeleteCommandParser : d -deactivate DeleteCommand +DeleteApplicantCommandParser --> DeleteCommandParser +deactivate DeleteApplicantCommandParser -DeleteCommandParser --> AddressBookParser : d +DeleteCommandParser -> DeleteApplicantCommandParser : parse("1") +activate DeleteApplicantCommandParser + +create DeleteApplicantCommand +DeleteApplicantCommandParser -> DeleteApplicantCommand +activate DeleteApplicantCommand + +DeleteApplicantCommand --> DeleteApplicantCommandParser : d +deactivate DeleteApplicantCommand + +DeleteApplicantCommandParser --> DeleteCommandParser : d +deactivate DeleteApplicantCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +DeleteApplicantCommandParser -[hidden]-> DeleteCommandParser +destroy DeleteApplicantCommandParser + +DeleteCommandParser --> HireLahParser : d deactivate DeleteCommandParser + 'Hidden arrow to position the destroy marker below the end of the activation bar. -DeleteCommandParser -[hidden]-> AddressBookParser +DeleteCommandParser -[hidden]-> HireLahParser destroy DeleteCommandParser -AddressBookParser --> LogicManager : d -deactivate AddressBookParser +HireLahParser --> LogicManager : d +deactivate HireLahParser -LogicManager -> DeleteCommand : execute() -activate DeleteCommand +LogicManager -> DeleteApplicantCommand : execute() +activate DeleteApplicantCommand -DeleteCommand -> Model : deletePerson(1) +DeleteApplicantCommand -> Model : getFilteredApplicantList() activate Model +Model --> DeleteApplicantCommand +deactivate Model -Model --> DeleteCommand +DeleteApplicantCommand -> Model : getApplicantInterviews(applicantToDelete) +activate Model +Model --> DeleteApplicantCommand deactivate Model +DeleteApplicantCommand -> Model : deleteApplicant(applicantToDelete) +activate Model +Model --> DeleteApplicantCommand +deactivate + +loop interviewsToDelete.size() + DeleteApplicantCommand -> Model : deleteInterview(i) + activate Model + Model --> DeleteApplicantCommand + deactivate +end + + + + + + create CommandResult -DeleteCommand -> CommandResult +DeleteApplicantCommand -> CommandResult activate CommandResult -CommandResult --> DeleteCommand +CommandResult --> DeleteApplicantCommand deactivate CommandResult -DeleteCommand --> LogicManager : result -deactivate DeleteCommand +DeleteApplicantCommand --> LogicManager : result +deactivate DeleteApplicantCommand [<--LogicManager deactivate LogicManager diff --git a/docs/diagrams/ExportSequenceDiagram.puml b/docs/diagrams/ExportSequenceDiagram.puml new file mode 100644 index 00000000000..a69f8e8509f --- /dev/null +++ b/docs/diagrams/ExportSequenceDiagram.puml @@ -0,0 +1,72 @@ +@startuml +!include style.puml +skinparam participantFontSize 25 +skinparam ArrowFontSize 30 +skinparam headerFontSize 30 + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":HireLahParser" as HireLahParser LOGIC_COLOR +participant ":ExportCsvCommandParser" as ExportCsvCommandParser LOGIC_COLOR +participant "L:ExportPositionCsvCommand" as ExportPositionCsvCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("export -p") +activate LogicManager + +LogicManager -> HireLahParser : parseCommand("export -p") +activate HireLahParser + +create ExportCsvCommandParser +HireLahParser -> ExportCsvCommandParser +activate ExportCsvCommandParser + +ExportCsvCommandParser --> HireLahParser +deactivate ExportCsvCommandParser + +HireLahParser -> ExportCsvCommandParser : parse("-p") +activate ExportCsvCommandParser + +create ExportPositionCsvCommand +ExportCsvCommandParser -> ExportPositionCsvCommand +activate ExportPositionCsvCommand + +ExportPositionCsvCommand --> ExportCsvCommandParser : L +deactivate ExportPositionCsvCommand + +ExportCsvCommandParser --> HireLahParser : L +deactivate ExportCsvCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ExportCsvCommandParser -[hidden]-> HireLahParser +destroy ExportCsvCommandParser + +HireLahParser --> LogicManager : L +deactivate HireLahParser + +LogicManager -> ExportPositionCsvCommand : execute() +activate ExportPositionCsvCommand + +ExportPositionCsvCommand -> Model : exportCsvPosition() +activate Model + +Model --> ExportPositionCsvCommand +deactivate Model + +create CommandResult +ExportPositionCsvCommand -> CommandResult +activate CommandResult + +CommandResult --> ExportPositionCsvCommand +deactivate CommandResult + +ExportPositionCsvCommand --> LogicManager : result +deactivate ExportPositionCsvCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/FilterSequenceDiagram.puml b/docs/diagrams/FilterSequenceDiagram.puml new file mode 100644 index 00000000000..cfdfd2c4819 --- /dev/null +++ b/docs/diagrams/FilterSequenceDiagram.puml @@ -0,0 +1,108 @@ +@startuml +!include style.puml + +skinparam participantFontSize 25 +skinparam ArrowFontSize 30 +skinparam headerFontSize 30 + +box LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":HireLahParser" as HireLahParser LOGIC_COLOR +participant ":ListCommandParser" as ListCommandParser LOGIC_COLOR +participant ":ListApplicantCommandParser" as ListApplicantCommandParser LOGIC_COLOR +participant "L:ListApplicantCommand" as ListApplicantCommand LOGIC_COLOR +participant "P:ApplicantNamePredicate" as ApplicantNamePredicate LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("list -a f/name a/Bob") +activate LogicManager + +LogicManager -> HireLahParser : parseCommand("list -a f/name a/Bob") +activate HireLahParser + +create ListCommandParser +HireLahParser -> ListCommandParser +activate ListCommandParser + +ListCommandParser --> HireLahParser +deactivate ListCommandParser + +HireLahParser -> ListCommandParser : parse("-a f/name a/Bob") +activate ListCommandParser + +create ListApplicantCommandParser +ListCommandParser -> ListApplicantCommandParser +activate ListApplicantCommandParser + +ListApplicantCommandParser --> ListCommandParser +deactivate ListApplicantCommandParser + +ListCommandParser -> ListApplicantCommandParser : parse("f/name a/Bob") +activate ListApplicantCommandParser + +ListApplicantCommandParser -> ListApplicantCommandParser : parseFilter("f/name a/Bob") +activate ListApplicantCommandParser + +create ListApplicantCommand +ListApplicantCommandParser -> ListApplicantCommand +activate ListApplicantCommand + +ListApplicantCommand --> ListApplicantCommandParser : L +deactivate ListApplicantCommand + +ListApplicantCommandParser --> ListApplicantCommandParser : L +deactivate ListApplicantCommandParser + +ListApplicantCommandParser --> ListCommandParser : L +deactivate ListApplicantCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ListApplicantCommandParser -[hidden]-> ListCommandParser +destroy ListApplicantCommandParser + +ListCommandParser --> HireLahParser : L +deactivate ListCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +ListCommandParser -[hidden]-> HireLahParser +destroy ListCommandParser + +HireLahParser --> LogicManager : L +deactivate HireLahParser + +LogicManager -> ListApplicantCommand : execute() +activate ListApplicantCommand + +ListApplicantCommand -> ListApplicantCommand : getFilterPredicate(name, Bob) +activate ListApplicantCommand + +create ApplicantNamePredicate +ListApplicantCommand -> ApplicantNamePredicate +activate ApplicantNamePredicate + +ApplicantNamePredicate --> ListApplicantCommand : P +deactivate ApplicantNamePredicate + +ListApplicantCommand --> ListApplicantCommand : P +deactivate ListApplicantCommand + +ListApplicantCommand -> Model : updateFilteredApplicantList(P) +activate Model + +Model --> ListApplicantCommand +deactivate Model + +create CommandResult +ListApplicantCommand -> CommandResult +activate CommandResult + +CommandResult --> ListApplicantCommand +deactivate CommandResult + +ListApplicantCommand --> LogicManager : result +deactivate ListApplicantCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/InterviewStatus.puml b/docs/diagrams/InterviewStatus.puml new file mode 100644 index 00000000000..0e183324870 --- /dev/null +++ b/docs/diagrams/InterviewStatus.puml @@ -0,0 +1,22 @@ +@startuml +'https://plantuml.com/activity-diagram-beta + +start +:Create Interview with Status: "Pending"; +if () then (pass) + :Change Status to: "Passed - waiting for applicant"; + :Increase number of position offers; + if () then (accept) + :Change Status to: "Accepted"; + :Update applicant job role; + :Decrease number of position openings; + else (reject) + :Change Status to "Rejected"; + endif + :Decrease number of position offers; +else (fail) + :Change Status to: "Failed"; +endif +stop + +@enduml diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index d4193173e18..0305ecd6624 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -6,7 +6,7 @@ skinparam classBackgroundColor LOGIC_COLOR package Logic { -Class AddressBookParser +Class HireLahParser Class XYZCommand Class CommandResult Class "{abstract}\nCommand" as Command @@ -27,8 +27,8 @@ Class HiddenOutside #FFFFFF HiddenOutside ..> Logic LogicManager .right.|> Logic -LogicManager -right->"1" AddressBookParser -AddressBookParser ..> XYZCommand : creates > +LogicManager -right->"1" HireLahParser +HireLahParser ..> XYZCommand : creates > XYZCommand -up-|> Command LogicManager .left.> Command : executes > @@ -38,7 +38,7 @@ LogicManager --> Storage Storage --[hidden] Model Command .[hidden]up.> Storage Command .right.> Model -note right of XYZCommand: XYZCommand = AddCommand, \nFindCommand, etc +note right of XYZCommand: XYZCommand = AddApplicantCommand, EditPositionCommand, etc Logic ..> CommandResult LogicManager .down.> CommandResult diff --git a/docs/diagrams/ModelApplicantClassDiagram.puml b/docs/diagrams/ModelApplicantClassDiagram.puml new file mode 100644 index 00000000000..5495c7e8a4e --- /dev/null +++ b/docs/diagrams/ModelApplicantClassDiagram.puml @@ -0,0 +1,43 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Class HireLah +Class ModelManager + +Package ApplicantDataType <>{ + +Class UniqueApplicantList +Class Applicant +Class Address +Class Email +Class Age +Class Gender +Class Name +Class Phone +Class HiredStatus +Class Tag +} + +ModelManager -->"~* filtered" Applicant + +HireLah *--> "1" UniqueApplicantList +UniqueApplicantList --> "~* all" Applicant +Applicant *--> Name +Applicant *--> Phone +Applicant *--> Email +Applicant *--> Age +Applicant *--> Gender +Applicant *--> Address +Applicant *--> HiredStatus +Applicant *--> "*" Tag + +Name -[hidden]right-> Phone +Phone -[hidden]right-> Address +Address -[hidden]right-> Email + +ModelManager -left-> "1" HireLah + +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 4439108973a..690fc883525 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -5,46 +5,30 @@ skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR Package Model <>{ -Class "<>\nReadOnlyAddressBook" as ReadOnlyAddressBook +Class "<>\nReadOnlyHireLah" as ReadOnlyHireLah Class "<>\nReadOnlyUserPrefs" as ReadOnlyUserPrefs Class "<>\nModel" as Model -Class AddressBook +Class HireLah Class ModelManager Class UserPrefs -Class UniquePersonList -Class Person -Class Address -Class Email -Class Name -Class Phone -Class Tag +Package DataType {} } Class HiddenOutside #FFFFFF HiddenOutside ..> Model -AddressBook .up.|> ReadOnlyAddressBook +HireLah .up.|> ReadOnlyHireLah ModelManager .up.|> Model Model .right.> ReadOnlyUserPrefs -Model .left.> ReadOnlyAddressBook -ModelManager -left-> "1" AddressBook +Model .left.> ReadOnlyHireLah +ModelManager -left-> "1" HireLah ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList -UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag +ModelManager -down-> "~* filtered" DataType +HireLah *-right-> "1" DataType -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email - -ModelManager -->"~* filtered" Person @enduml diff --git a/docs/diagrams/ModelInterviewClassDiagram.puml b/docs/diagrams/ModelInterviewClassDiagram.puml new file mode 100644 index 00000000000..e5955732ab2 --- /dev/null +++ b/docs/diagrams/ModelInterviewClassDiagram.puml @@ -0,0 +1,32 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Class HireLah +Class ModelManager + +Package InterviewDataType <>{ + +Class UniqueInterviewList +Class Interview +Class Applicant +Class Position +Class Status +} + +ModelManager -->"~* filtered" Interview + +HireLah *--> "1" UniqueInterviewList +UniqueInterviewList --> "~* all" Interview +Interview *--> Status +Interview *--> "1" Applicant +Interview *--> "1" Position + +Applicant -[hidden]right-> Position +Position -[hidden]right-> Status + +ModelManager -right-> "1" HireLah + +@enduml diff --git a/docs/diagrams/ModelPositionClassDiagram.puml b/docs/diagrams/ModelPositionClassDiagram.puml new file mode 100644 index 00000000000..c9da61b3221 --- /dev/null +++ b/docs/diagrams/ModelPositionClassDiagram.puml @@ -0,0 +1,35 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Class HireLah +Class ModelManager + +Package PositionDataType <>{ + +Class UniquePositionList +Class Position +Class Description +Class PositionName +Class PositionOffers +Class PositionOpenings +Class Requirement +} + +ModelManager -->"~* filtered" Position + +HireLah *--> "1" UniquePositionList +UniquePositionList --> "~* all" Position +Position *--> Description +Position *--> PositionName +Position *--> PositionOffers +Position *--> PositionOpenings +Position *--> "*" Requirement + +ModelManager -left-> "1" HireLah + +PositionName -[hidden]right-> Description + +@enduml diff --git a/docs/diagrams/ParserClasses.puml b/docs/diagrams/ParserClasses.puml index 0c7424de6e0..957bad49a50 100644 --- a/docs/diagrams/ParserClasses.puml +++ b/docs/diagrams/ParserClasses.puml @@ -9,7 +9,7 @@ Class XYZCommand package "Parser classes"{ Class "<>\nParser" as Parser -Class AddressBookParser +Class HireLahParser Class XYZCommandParser Class CliSyntax Class ParserUtil @@ -19,12 +19,12 @@ Class Prefix } Class HiddenOutside #FFFFFF -HiddenOutside ..> AddressBookParser +HiddenOutside ..> HireLahParser -AddressBookParser .down.> XYZCommandParser: creates > +HireLahParser .down.> XYZCommandParser: creates > XYZCommandParser ..> XYZCommand : creates > -AddressBookParser ..> Command : returns > +HireLahParser ..> Command : returns > XYZCommandParser .up.|> Parser XYZCommandParser ..> ArgumentMultimap XYZCommandParser ..> ArgumentTokenizer diff --git a/docs/diagrams/SortSequenceDiagram.puml b/docs/diagrams/SortSequenceDiagram.puml new file mode 100644 index 00000000000..a02625bd134 --- /dev/null +++ b/docs/diagrams/SortSequenceDiagram.puml @@ -0,0 +1,104 @@ +@startuml +!include style.puml +skinparam participantFontSize 25 +skinparam ArrowFontSize 30 +skinparam headerFontSize 30 + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":HireLahParser" as HireLahParser LOGIC_COLOR +participant ":ListCommandParser" as ListCommandParser LOGIC_COLOR +participant ":ListApplicantCommandParser" as ListApplicantCommandParser LOGIC_COLOR +participant "L:ListApplicantCommand" as ListApplicantCommand LOGIC_COLOR +participant "P:ApplicantNameComparator" as ApplicantNameComparator LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("list -a s/asc") +activate LogicManager + +LogicManager -> HireLahParser : parseCommand("list -a s/asc") +activate HireLahParser + +create ListCommandParser +HireLahParser -> ListCommandParser +activate ListCommandParser + +ListCommandParser --> HireLahParser +deactivate ListCommandParser + +HireLahParser -> ListCommandParser : parse("-a s/asc") +activate ListCommandParser + +create ListApplicantCommandParser +ListCommandParser -> ListApplicantCommandParser +activate ListApplicantCommandParser + +ListApplicantCommandParser --> ListCommandParser +deactivate ListApplicantCommandParser + +ListCommandParser -> ListApplicantCommandParser : parse("s/asc") +activate ListApplicantCommandParser + +ListApplicantCommandParser -> ListApplicantCommandParser : parseSort("s/asc") +activate ListApplicantCommandParser + +create ListApplicantCommand +ListApplicantCommandParser -> ListApplicantCommand +activate ListApplicantCommand + +ListApplicantCommand --> ListApplicantCommandParser : L +deactivate ListApplicantCommand + +ListApplicantCommandParser --> ListApplicantCommandParser : L +deactivate ListApplicantCommandParser + +ListApplicantCommandParser --> ListCommandParser : L +deactivate ListApplicantCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ListApplicantCommandParser -[hidden]-> ListCommandParser +destroy ListApplicantCommandParser + +ListCommandParser --> HireLahParser : L +deactivate ListCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +ListCommandParser -[hidden]-> HireLahParser +destroy ListCommandParser + +HireLahParser --> LogicManager : L +deactivate HireLahParser + +LogicManager -> ListApplicantCommand : execute() +activate ListApplicantCommand + +create ApplicantNameComparator +ListApplicantCommand -> ApplicantNameComparator +activate ApplicantNameComparator + +ApplicantNameComparator --> ListApplicantCommand : P +deactivate ApplicantNameComparator + +ListApplicantCommand -> Model : updateSortApplicantList(P) +activate Model + +Model --> ListApplicantCommand +deactivate Model + +create CommandResult +ListApplicantCommand -> CommandResult +activate CommandResult + +CommandResult --> ListApplicantCommand +deactivate CommandResult + +ListApplicantCommand --> LogicManager : result +deactivate ListApplicantCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index 760305e0e58..60771f975f5 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -4,7 +4,7 @@ skinparam arrowThickness 1.1 skinparam arrowColor STORAGE_COLOR skinparam classBackgroundColor STORAGE_COLOR -package Storage{ +package Storage { package "UserPrefs Storage" #F4F6F6{ Class "<>\nUserPrefsStorage" as UserPrefsStorage @@ -14,12 +14,15 @@ Class JsonUserPrefsStorage Class "<>\nStorage" as Storage Class StorageManager -package "AddressBook Storage" #F4F6F6{ -Class "<>\nAddressBookStorage" as AddressBookStorage -Class JsonAddressBookStorage -Class JsonSerializableAddressBook -Class JsonAdaptedPerson +package "HireLah Storage" #F4F6F6{ +Class "<>\nHireLahStorage" as HireLahStorage +Class JsonHireLahStorage +Class JsonSerializableHireLah +Class JsonAdaptedApplicant Class JsonAdaptedTag +Class JsonAdaptedInterview +Class JsonAdaptedPosition +Class JsonAdaptedRequirement } } @@ -29,15 +32,21 @@ HiddenOutside ..> Storage StorageManager .up.|> Storage StorageManager -up-> "1" UserPrefsStorage -StorageManager -up-> "1" AddressBookStorage +StorageManager -up-> "1" HireLahStorage Storage -left-|> UserPrefsStorage -Storage -right-|> AddressBookStorage +Storage -right-|> HireLahStorage JsonUserPrefsStorage .up.|> UserPrefsStorage -JsonAddressBookStorage .up.|> AddressBookStorage -JsonAddressBookStorage ..> JsonSerializableAddressBook -JsonSerializableAddressBook --> "*" JsonAdaptedPerson -JsonAdaptedPerson --> "*" JsonAdaptedTag - +JsonHireLahStorage -up|> HireLahStorage +JsonHireLahStorage ..> JsonSerializableHireLah +JsonSerializableHireLah -d> "*" JsonAdaptedApplicant +JsonAdaptedApplicant --> "*" JsonAdaptedTag +JsonSerializableHireLah --> "*" JsonAdaptedInterview +JsonSerializableHireLah --> "*" JsonAdaptedPosition +JsonAdaptedPosition --> "*" JsonAdaptedRequirement +JsonAdaptedInterview --> "1" JsonAdaptedApplicant +JsonAdaptedInterview --> "1" JsonAdaptedPosition +JsonAdaptedInterview -right[hidden]-> JsonAdaptedPosition +JsonAdaptedApplicant -right[hidden]-> JsonAdaptedApplicant @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..ba373e75ca1 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -3,6 +3,7 @@ skinparam arrowThickness 1.1 skinparam arrowColor UI_COLOR_T4 skinparam classBackgroundColor UI_COLOR +skinparam classFontSize 16 package UI <>{ Class "<>\nUi" as Ui @@ -11,8 +12,12 @@ Class UiManager Class MainWindow Class HelpWindow Class ResultDisplay -Class PersonListPanel -Class PersonCard +Class ApplicantListPanel +Class ApplicantCard +Class PositionListPanel +Class PositionCard +Class InterviewListPanel +Class InterviewCard Class StatusBarFooter Class CommandBox } @@ -32,26 +37,36 @@ UiManager .left.|> Ui UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay -MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" ApplicantListPanel +MainWindow *-down-> "1" PositionListPanel +MainWindow *-down-> "1" InterviewListPanel MainWindow *-down-> "1" StatusBarFooter -MainWindow --> "0..1" HelpWindow +MainWindow ---> "0..1" HelpWindow -PersonListPanel -down-> "*" PersonCard +ApplicantListPanel -down-> "*" ApplicantCard +PositionListPanel -down-> "*" PositionCard +InterviewListPanel -down-> "*" InterviewCard MainWindow -left-|> UiPart -ResultDisplay --|> UiPart -CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart -StatusBarFooter --|> UiPart -HelpWindow --|> UiPart +ResultDisplay ---|> UiPart +CommandBox ---|> UiPart +ApplicantListPanel ---|> UiPart +ApplicantCard ---|> UiPart +PositionListPanel ---|> UiPart +PositionCard ---|> UiPart +InterviewListPanel ---|> UiPart +InterviewCard ---|> UiPart +StatusBarFooter ---|> UiPart +HelpWindow -down--|> UiPart -PersonCard ..> Model +ApplicantCard ....> Model +PositionCard ....> Model +InterviewCard ....> Model UiManager -right-> Logic MainWindow -left-> Logic -PersonListPanel -[hidden]left- HelpWindow +ApplicantListPanel -[hidden]left- HelpWindow HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay ResultDisplay -[hidden]left- StatusBarFooter diff --git a/docs/diagrams/UndoRedoState0.puml b/docs/diagrams/UndoRedoState0.puml deleted file mode 100644 index 96e30744d24..00000000000 --- a/docs/diagrams/UndoRedoState0.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title Initial state - -package States { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 -hide State2 -hide State3 - -class Pointer as "Current State" #FFFFF -Pointer -up-> State1 -@end diff --git a/docs/diagrams/UndoRedoState1.puml b/docs/diagrams/UndoRedoState1.puml deleted file mode 100644 index 01fcb9b2b96..00000000000 --- a/docs/diagrams/UndoRedoState1.puml +++ /dev/null @@ -1,22 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "delete 5" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -hide State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState2.puml b/docs/diagrams/UndoRedoState2.puml deleted file mode 100644 index bccc230a5d1..00000000000 --- a/docs/diagrams/UndoRedoState2.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "add n/David" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State3 -@end diff --git a/docs/diagrams/UndoRedoState3.puml b/docs/diagrams/UndoRedoState3.puml deleted file mode 100644 index ea29c9483e4..00000000000 --- a/docs/diagrams/UndoRedoState3.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "undo" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState4.puml b/docs/diagrams/UndoRedoState4.puml deleted file mode 100644 index 1b784cece80..00000000000 --- a/docs/diagrams/UndoRedoState4.puml +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "list" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab2:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState5.puml b/docs/diagrams/UndoRedoState5.puml deleted file mode 100644 index 88927be32bc..00000000000 --- a/docs/diagrams/UndoRedoState5.puml +++ /dev/null @@ -1,21 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 - -title After command "clear" - -package States <> { - class State1 as "__ab0:AddressBook__" - class State2 as "__ab1:AddressBook__" - class State3 as "__ab3:AddressBook__" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFF - -Pointer -up-> State3 -note right on link: State ab2 deleted. -@end diff --git a/docs/diagrams/UndoSequenceDiagram.puml b/docs/diagrams/UndoSequenceDiagram.puml deleted file mode 100644 index 410aab4e412..00000000000 --- a/docs/diagrams/UndoSequenceDiagram.puml +++ /dev/null @@ -1,53 +0,0 @@ -@startuml -!include style.puml - -box Logic LOGIC_COLOR_T1 -participant ":LogicManager" as LogicManager LOGIC_COLOR -participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR -participant "u:UndoCommand" as UndoCommand LOGIC_COLOR -end box - -box Model MODEL_COLOR_T1 -participant ":Model" as Model MODEL_COLOR -participant ":VersionedAddressBook" as VersionedAddressBook MODEL_COLOR -end box -[-> LogicManager : execute(undo) -activate LogicManager - -LogicManager -> AddressBookParser : parseCommand(undo) -activate AddressBookParser - -create UndoCommand -AddressBookParser -> UndoCommand -activate UndoCommand - -UndoCommand --> AddressBookParser -deactivate UndoCommand - -AddressBookParser --> LogicManager : u -deactivate AddressBookParser - -LogicManager -> UndoCommand : execute() -activate UndoCommand - -UndoCommand -> Model : undoAddressBook() -activate Model - -Model -> VersionedAddressBook : undo() -activate VersionedAddressBook - -VersionedAddressBook -> VersionedAddressBook :resetData(ReadOnlyAddressBook) -VersionedAddressBook --> Model : -deactivate VersionedAddressBook - -Model --> UndoCommand -deactivate Model - -UndoCommand --> LogicManager : result -deactivate UndoCommand -UndoCommand -[hidden]-> LogicManager : result -destroy UndoCommand - -[<--LogicManager -deactivate LogicManager -@enduml diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index fad8b0adeaa..2027fd740e9 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -53,10 +53,17 @@ skinparam Actor { skinparam Sequence { MessageAlign center - BoxFontSize 15 + BoxFontSize 20 BoxPadding 0 BoxFontColor #FFFFFF FontName Arial + ParticipantFontSize 25 + ArrowFontSize 20 + GroupFontSize 20 + GroupHeaderFontSize 15 + GroupBodyBackGroundColor #F2F2E2 + ArrowThickness 3 + LifeLineBorderThickness 3 } skinparam Participant { diff --git a/docs/diagrams/tracing/LogicSequenceDiagram.puml b/docs/diagrams/tracing/LogicSequenceDiagram.puml index fdcbe1c0ccc..c78d171f931 100644 --- a/docs/diagrams/tracing/LogicSequenceDiagram.puml +++ b/docs/diagrams/tracing/LogicSequenceDiagram.puml @@ -13,7 +13,7 @@ create ecp abp -> ecp abp -> ecp ++: parse(arguments) create ec -ecp -> ec ++: index, editPersonDescriptor +ecp -> ec ++: index, editApplicantDescriptor ec --> ecp -- ecp --> abp --: command abp --> logic --: command diff --git a/docs/images/AddParser.png b/docs/images/AddParser.png new file mode 100644 index 00000000000..de8c29c31a1 Binary files /dev/null and b/docs/images/AddParser.png differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 2f1346869d0..2eef15badb7 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png deleted file mode 100644 index 1ec62caa2a5..00000000000 Binary files a/docs/images/BetterModelClassDiagram.png and /dev/null differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index fa327b39618..f88a5c82f63 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/ExportSequenceDiagram.png b/docs/images/ExportSequenceDiagram.png new file mode 100644 index 00000000000..cd4e2539ebd Binary files /dev/null and b/docs/images/ExportSequenceDiagram.png differ diff --git a/docs/images/FilterSequenceDiagram.png b/docs/images/FilterSequenceDiagram.png new file mode 100644 index 00000000000..844b0a9b836 Binary files /dev/null and b/docs/images/FilterSequenceDiagram.png differ diff --git a/docs/images/InterviewStatus.png b/docs/images/InterviewStatus.png new file mode 100644 index 00000000000..b5079f41b3d Binary files /dev/null and b/docs/images/InterviewStatus.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index 9e9ba9f79e5..8d08368c3b3 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/ModelApplicantClassDiagram.png b/docs/images/ModelApplicantClassDiagram.png new file mode 100644 index 00000000000..1e633959331 Binary files /dev/null and b/docs/images/ModelApplicantClassDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 04070af60d8..c448bd9bb31 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/ModelInterviewClassDiagram.png b/docs/images/ModelInterviewClassDiagram.png new file mode 100644 index 00000000000..928d7cdde99 Binary files /dev/null and b/docs/images/ModelInterviewClassDiagram.png differ diff --git a/docs/images/ModelPositionClassDiagram.png b/docs/images/ModelPositionClassDiagram.png new file mode 100644 index 00000000000..72533e5defd Binary files /dev/null and b/docs/images/ModelPositionClassDiagram.png differ diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png index e7b4c8880cd..0e9965fa97e 100644 Binary files a/docs/images/ParserClasses.png and b/docs/images/ParserClasses.png differ diff --git a/docs/images/SortSequenceDiagram.png b/docs/images/SortSequenceDiagram.png new file mode 100644 index 00000000000..3a11328eb34 Binary files /dev/null and b/docs/images/SortSequenceDiagram.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 2533a5c1af0..725f4f4eb7f 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..7d9a83c8812 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 785e04dbab4..d83edfb97fe 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoRedoState0.png b/docs/images/UndoRedoState0.png deleted file mode 100644 index 8f7538cd884..00000000000 Binary files a/docs/images/UndoRedoState0.png and /dev/null differ diff --git a/docs/images/UndoRedoState1.png b/docs/images/UndoRedoState1.png deleted file mode 100644 index df9908d0948..00000000000 Binary files a/docs/images/UndoRedoState1.png and /dev/null differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png deleted file mode 100644 index 36519c1015b..00000000000 Binary files a/docs/images/UndoRedoState2.png and /dev/null differ diff --git a/docs/images/UndoRedoState3.png b/docs/images/UndoRedoState3.png deleted file mode 100644 index 19959d01712..00000000000 Binary files a/docs/images/UndoRedoState3.png and /dev/null differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png deleted file mode 100644 index 4c623e4f2c5..00000000000 Binary files a/docs/images/UndoRedoState4.png and /dev/null differ diff --git a/docs/images/UndoRedoState5.png b/docs/images/UndoRedoState5.png deleted file mode 100644 index 84ad2afa6bd..00000000000 Binary files a/docs/images/UndoRedoState5.png and /dev/null differ diff --git a/docs/images/UndoSequenceDiagram.png b/docs/images/UndoSequenceDiagram.png deleted file mode 100644 index 6addcd3a8d9..00000000000 Binary files a/docs/images/UndoSequenceDiagram.png and /dev/null differ diff --git a/docs/images/goalfix.png b/docs/images/goalfix.png new file mode 100644 index 00000000000..83a26669e81 Binary files /dev/null and b/docs/images/goalfix.png differ diff --git a/docs/images/khoahre123.png b/docs/images/khoahre123.png new file mode 100644 index 00000000000..ecace9f64e0 Binary files /dev/null and b/docs/images/khoahre123.png differ diff --git a/docs/images/likeabowx.png b/docs/images/likeabowx.png new file mode 100644 index 00000000000..a34d7d26da6 Binary files /dev/null and b/docs/images/likeabowx.png differ diff --git a/docs/images/sethckl.png b/docs/images/sethckl.png new file mode 100644 index 00000000000..8ddb9a28b52 Binary files /dev/null and b/docs/images/sethckl.png differ diff --git a/docs/images/yihern-lee.png b/docs/images/yihern-lee.png new file mode 100644 index 00000000000..c4f9454cff6 Binary files /dev/null and b/docs/images/yihern-lee.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..1f8c1302445 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,18 @@ --- layout: page -title: AddressBook Level-3 +title: HireLah --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![Java CI](https://github.com/AY2122S2-CS2103-W17-4/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2122S2-CS2103-W17-4/tp/actions/workflows/gradle.yml) +[![codecov](https://codecov.io/gh/AY2122S2-CS2103-W17-4/tp/branch/master/graph/badge.svg?token=9JSGEC8BP1)](https://codecov.io/gh/AY2122S2-CS2103-W17-4/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). +HireLah is a desktop app that helps **recruiters to manage talent and job candidates** by tracking every step of the hiring process, +from offering positions to scheduling interviews with candidates. It is optimised for Command Line Interface (CLI) users while still offering a GUI, so that power users can accomplish tasks much quicker by using commands -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** -* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org) diff --git a/docs/team/goalfix.md b/docs/team/goalfix.md new file mode 100644 index 00000000000..fdb6bc1471f --- /dev/null +++ b/docs/team/goalfix.md @@ -0,0 +1,59 @@ +--- +layout: page +title: Wei Howe's Project Portfolio Page +--- + +### Project: HireLah + +### Overview +HireLah is a desktop app that helps recruiters to manage talent and job candidates by tracking every step of the hiring +process, from offering positions to scheduling interviews with candidates. It is optimised for Command Line Interface +(CLI) users while still offering a GUI, so that power users can accomplish tasks much quicker by using commands . It is +written in Java, and has about 10 kLoC. + +### Summary of Contributions: +Given below are my contributions to the project. + +* **New Feature**: `Interviews` + * New Interview data type to represent an `Applicant` + attending an `Interview` for a `Position`. + * A successful interview allows the `Applicant` to be + matched with an open `Position`, if the applicant agrees to + `accept` the interview. Core functionality of HireLah which + which is do hiring and matching. + * `add` / `delete` / `pass` / `fail` / `accept` / `reject` + commands for interview. + + +* Other notable contributions to support this feature: + * Multiple Parsers to support various commands for Interviews. + * Methods in `Model` to support the cascading effects for `passing`/`accepting` an Interview. + * Some unit testing to ensure that commands work as intended. + + +* **Code contributed**: Over 1900 LoC [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=goalfix&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByAuthors&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-02-18) + + +* **Project management**: + * Main in charge of setting up issues in the Github issue tracker, + and creating milestones for the team. + * Reviewed 52 PRs. + * Left constructive feedback on several PRs, including but not limited to [#76](https://github.com/AY2122S2-CS2103-W17-4/tp/pull/76), + [#154](https://github.com/AY2122S2-CS2103-W17-4/tp/pull/154) [#158](https://github.com/AY2122S2-CS2103-W17-4/tp/pull/158). + * Helped out team with [release management](https://github.com/AY2122S2-CS2103-W17-4/tp/releases) for v1.3.1. + * Provided git CLI advise and help to teammates. + + +* **Documentation**: + * [User Guide](https://ay2122s2-cs2103-w17-4.github.io/tp/UserGuide.html): + * Added documentation for the features of `interview`, including + `add -i`, `delete -i`, `pass`, `fail`, `accept`, `reject`. + * [Developer Guide](https://ay2122s2-cs2103-w17-4.github.io/tp/DeveloperGuide.html): + * Added use case for adding applicants. + * Updated sequence diagram for architecture overview to reflect renamed classes. + * Added activity diagram and explanation for tracking interview status. + * Added sequence diagram and design consideration for adding of data. + +* **Community**: + * Contributed [16 issues](https://github.com/goalfix/ped/issues) during PE dry run. + * Contributed 9 posts to the module forum, including [reporting a potential bug](https://github.com/nus-cs2103-AY2122S2/forum/issues/228). diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index 773a07794e2..00000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -layout: page -title: John Doe's Project Portfolio Page ---- - -### Project: AddressBook Level 3 - -AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. - -Given below are my contributions to the project. - -* **New Feature**: Added the ability to undo/redo previous commands. - * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. - * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. - * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. - * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* - -* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. - -* **Code contributed**: [RepoSense link]() - -* **Project management**: - * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub - -* **Enhancements to existing features**: - * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) - * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) - -* **Documentation**: - * User Guide: - * Added documentation for the features `delete` and `find` [\#72]() - * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() - * Developer Guide: - * Added implementation details of the `delete` feature. - -* **Community**: - * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() - * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) - * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) - * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) - -* **Tools**: - * Integrated a third party library (Natty) to the project ([\#42]()) - * Integrated a new Github plugin (CircleCI) to the team repo - -* _{you can add/remove categories in the list above}_ diff --git a/docs/team/khoahre123.md b/docs/team/khoahre123.md new file mode 100644 index 00000000000..f7cbec1b0f4 --- /dev/null +++ b/docs/team/khoahre123.md @@ -0,0 +1,60 @@ +--- +layout: page +title: Khoa's Project Portfolio Page +--- + +### Project: HireLah + +HireLah is a CLI-based app that offers help for recruiters who need to managed hundreds of applicants per day. +By having short and simple syntax, HireLah helps to accelerate the normal scheduling and filtering process. +The app is most suitable for CLI-preferred users, but GUI is also provided to choose. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Sort feature in `list` command + * Added new optional parameters to the list command to allow sorting of different data types. + * This allows user to easily view the data with respect of the first and last occurrence. + * Justification: The user may pose a need to organize the data, or view the data from bottom up or top down. +`sort` feature helps user can perform the action quickly when having a large dataset. + * Highlights: The `sort` command sort the internal storing list, instead of the viewing list. +Hence, the sorting effect can retain after user close and reopen HireLah. + +* **New Feature**: Export CSV command + * New command allows user to export the HireLah data into a CSV file. + * This gives user ability to perform other operations on `applicants`, `interview` and `position` data in their +respective CSV files. + * Justification: Although HireLah target user is recruiters, they will also need to submit report to their respective +supervisors. Therefore, an appropriate command is needed to capture the need of the target user. As company typically +uses Excel as a mean of reporting, `export` command output the data into CSV file, which can then easily open and +manipulate by Excel. + * Highlights: The `export` command only output the data currently displayed in HireLah. Thus, user +can perform operation like `filter` or `sort` to get the desired data before `export`. + +* **Code contributed**: + * Contributed [Over 1500+ LoC](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=khoahre123&breakdown=true&sort=groupTitle&sortWithin=title&since=2022-02-18&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + to the project + +* **Project management**: + * Managed `Issues` and `Milestone` progress before each iteration. + * Updated meeting minutes in weekly team meetings. + * Started a mock testing session to increase quality of the product. + +* **Enhancements to existing features**: + * Extended the storage to stores `interview` and `position` + * Updated description for `help` command, include adding new description for new commands + * Modified `HelpWindow` to displayed specific description for each commands. + +* **Documentation**: + * User Guide: + * Added documentation for the feature `help` + * Added documentation for the features sort in `list` command + * Added documentation for the feature `export` + * Developer Guide: + * Updated class diagram `Storage` component and their descriptions + * Added use cases for viewing help, sorting and exporting data + * Updated implementation details of `sort` feature and added UML Sequence diagram as an example. + +* **Community**: + * Reviewed PRs within the team (Over [30 PRs](https://github.com/AY2122S2-CS2103-W17-4/tp/pulls?q=is%3Apr+is%3Aclosed+reviewed-by%3Akhoahre123)) + * Contributed [10 issues](https://github.com/khoahre123/ped/issues) during PE dry run + diff --git a/docs/team/likeabowx.md b/docs/team/likeabowx.md new file mode 100644 index 00000000000..90d65661144 --- /dev/null +++ b/docs/team/likeabowx.md @@ -0,0 +1,48 @@ +--- +layout: page +title: Bryan Ong's Project Portfolio Page +--- + +### Project: HireLah + +HireLah is a desktop app that helps recruiters to manage talent and job applicants by tracking every step of the hiring process, from offering positions to scheduling interviews with candidates. It is optimised for Command Line Interface (CLI) users while still offering a GUI, so that power users can accomplish tasks much quicker by using commands . It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Tabs in UI + * New tabs in UI to display the list of `applicants`, `positions` and `interviews` + * This allows user to view the three lists simultaneously, which makes it easier to reference the indexes + + +* **New Feature**: Filter feature in `list` command + * Added new optional parameters to the `list` command to allow filtering of data using different filter criteria + * This allows user to easily find the data they are looking for + + +* **Code contributed**: + * Contributed over [3000+ LoC](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=likeabowx&sort=groupTitle&sortWithin=title&since=2022-02-18&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=false) to the project + + +* **Project management**: + * Reviewed over [50 PRs](https://github.com/AY2122S2-CS2103-W17-4/tp/pulls?q=is%3Apr+is%3Aclosed+reviewed-by%3Alikeabowx) within the team + + +* **Enhancements to existing features**: + * Added parsing of flags for `add`, `edit`, `delete`, `list` commands to perform operations to different data types + * Extended the `edit` command to `edit -a` for editing applicants and `edit -i` for editing interviews + * Changed the overall UI style + + +* **Documentation**: + * [User Guide](https://ay2122s2-cs2103-w17-4.github.io/tp/UserGuide.html): + * Added documentation for `list` command and filter-related features in User Guide + * Added FAQ in User Guide + * [Developer Guide](https://ay2122s2-cs2103-w17-4.github.io/tp/DeveloperGuide.html): + * Updated class diagram `Ui` and `Model` component and their descriptions + * Added use cases for filtering data + * Updated list of non-functional requirements (NFRs) + * Added instructions for manual testing + + +* **Community**: + * Contributed [7 issues](https://github.com/likeabowx/ped/issues) during PE dry run diff --git a/docs/team/sethckl.md b/docs/team/sethckl.md new file mode 100644 index 00000000000..f941e7b492a --- /dev/null +++ b/docs/team/sethckl.md @@ -0,0 +1,52 @@ +--- +layout: page +title: Kok Leong's Project Portfolio Page +--- + +### Project: HireLah + +HireLah is a desktop app that helps recruiters to manage talent and job candidates by tracking every step of the hiring +process, from offering positions to scheduling interviews with candidates. It is optimised for Command Line Interface +(CLI) users while still offering a GUI, so that power users can accomplish tasks much quicker by using commands . It is +written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Applicant + * New Applicant data type to represent job applicants in HireLah + * contains new Gender and Age attributes to facilitate Human Resource needs. + * contains new HiredStatus property which is referenced by Interviews and Positions for functionality. + +* Other notable contributions: + * Added and updated sample test data to support new Gender and Age attributes for Applicant. + * Added and updated unit tests for Applicant. + * Added and updated unit tests for Storage. + * Refactored classes and parameters with Persons to Applicant in the codebase to reflect evolving AB-3 to + HireLah. + + +* **Code contributed**: + * Contributed over [1700+ LoC](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=SethCKL&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-02-18) + +* **Project management**: + * Reviewed 29 PRs. + +* **Documentation**: + * User Guide: + * Added documentation for the features `add -a`, `edit -a`, `del -a` + * Edited language and grammar to improve clarity and readability. + * Formatted structure to improve organisation of information. + * Developer Guide: + * Explanation for the implementation of Applicant + * Added user stories + * Glossary of terms to improve common understanding. + * Proof read to ensure language consistency. + +* **Team-based tasks** + * Refactored classes, parameters and tests with AddressBook to HireLah in the codebase to reflect evolving + AB-3 to HireLah. + +* **Community**: + * Contributed [3 issues](https://github.com/SethCKL/ped/issues) during PE dry run + + diff --git a/docs/team/yihern-lee.md b/docs/team/yihern-lee.md new file mode 100644 index 00000000000..48125fad58e --- /dev/null +++ b/docs/team/yihern-lee.md @@ -0,0 +1,53 @@ +--- +layout: page +title: Yi Hern's Project Portfolio Page +--- + +### Project: HireLah + +HireLah is a desktop app that helps recruiters to manage talent and job candidates by tracking every step of the hiring process, from offering positions to scheduling interviews with candidates. It is optimised for Command Line Interface (CLI) users while still offering a GUI, so that power users can accomplish tasks much quicker by using commands . It is written in Java, and has about 10 kLoC. + +### Summary of Contributions: +Given below are my contributions to the project. + +* **New Feature**: Position + * New Position data type to represent a position in the company that is open for hire. + * Position tracks the number of openings, as well as, the number of offers handed out the applicants. + * Position contains other fields, such as, description and requirement tags, to allow recruiters to associate information with the position. + * Other notable contributions to support this feature: + * Parsers to support `add`, `delete` and `edit` commands for Positions. + * Command logic for executing `add`, `delete` and `edit`. + * Methods in `Model` to support the cascading effects for editing a Position. + * Unit tests to cover the input validation for `PositionName`, `PositionOpenings`, `Description` and `Requirement`. + * Unit tests to cover the execution correctness for `AddPositionCommand`, `DeletePositionCommand`, `EditPositionCommand` and their various parsers. +* **Code contributed**: Contributed over [3,000+ L0C](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=yihern-lee&breakdown=true&sort=groupTitle&sortWithin=title&since=2022-02-18&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) +to the project. + +* **Project management**: + * Reviewed 23 PRs. + * Left constructive feedbacks on PRs [#76](https://github.com/AY2122S2-CS2103-W17-4/tp/pull/76), [#83](https://github.com/AY2122S2-CS2103-W17-4/tp/pull/83), + [#109](https://github.com/AY2122S2-CS2103-W17-4/tp/pull/109), [#156](https://github.com/AY2122S2-CS2103-W17-4/tp/pull/156), + [#158](https://github.com/AY2122S2-CS2103-W17-4/tp/pull/158). + * Set up issue for main v1.3 feature of tracking interview status. [#122](https://github.com/AY2122S2-CS2103-W17-4/tp/issues/122) + +* **Enhancements to existing features**: + * Made changed or advised on changes to the validation regex for fields in Applicant (refactored from Person). + * Regex to ensure that at least one alphabet exists in `Name` and length is shorter than 100 characters. + * Enhancements to `ModelManager` to support cascading effects of editing `Position` and `Applicant`. + +* **Documentation**: + * User Guide: + * Add documentation for the features `add -p`, `edit -p`, and `delete -p`. + * Contributions to explain the validation logic in regard to `Position`, under `pass` and `accept` commands. + * Developer Guide: + * Use case for editing position. + * Explanation for the implementation of Position. + * Sequence diagram for `DeleteApplicantCommand`. + * Update UML diagram to reflect new class name `HireLah` instead of `AddressBook`. [#373](https://github.com/AY2122S2-CS2103-W17-4/tp/pull/373) + * Add manual testing instructions for `add` command. [#370](https://github.com/AY2122S2-CS2103-W17-4/tp/pull/370) + +* **Community**: + * Contribute 15 bug reports for PE dry run. + * Forum contributions: + * Question to enhance the closing process for iP chatbot project [#127](https://github.com/nus-cs2103-AY2122S2/forum/issues/127). + * Contributed to discussion on the immutable design for data classes in tP [#193](https://github.com/nus-cs2103-AY2122S2/forum/issues/193). diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md index 880c701042f..0e40064d838 100644 --- a/docs/tutorials/AddRemark.md +++ b/docs/tutorials/AddRemark.md @@ -28,7 +28,7 @@ package seedu.address.logic.commands; import seedu.address.model.Model; /** - * Changes the remark of an existing person in the address book. + * Changes the remark of an existing applicant in the address book. */ public class RemarkCommand extends Command { @@ -65,8 +65,8 @@ Following the convention in other commands, we add relevant messages as constant ``` java public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Edits the remark of the person identified " - + "by the index number used in the last person listing. " + + ": Edits the remark of the applicant identified " + + "by the index number used in the last applicant listing. " + "Existing remark will be overwritten by the input.\n" + "Parameters: INDEX (must be a positive integer) " + "r/ [REMARK]\n" @@ -101,8 +101,8 @@ public class RemarkCommand extends Command { private final String remark; /** - * @param index of the person in the filtered person list to edit the remark - * @param remark of the person to be updated to + * @param index of the applicant in the filtered applicant list to edit the remark + * @param remark of the applicant to be updated to */ public RemarkCommand(Index index, String remark) { requireAllNonNull(index, remark); @@ -225,11 +225,11 @@ If you are stuck, check out the sample ## Add `Remark` to the model -Now that we have all the information that we need, let’s lay the groundwork for propagating the remarks added into the in-memory storage of person data. We achieve that by working with the `Person` model. Each field in a Person is implemented as a separate class (e.g. a `Name` object represents the person’s name). That means we should add a `Remark` class so that we can use a `Remark` object to represent a remark given to a person. +Now that we have all the information that we need, let’s lay the groundwork for propagating the remarks added into the in-memory storage of applicant data. We achieve that by working with the `Person` model. Each field in a Person is implemented as a separate class (e.g. a `Name` object represents the applicant’s name). That means we should add a `Remark` class so that we can use a `Remark` object to represent a remark given to a applicant. ### Add a new `Remark` class -Create a new `Remark` in `seedu.address.model.person`. Since a `Remark` is a field that is similar to `Address`, we can reuse a significant bit of code. +Create a new `Remark` in `seedu.address.model.applicant`. Since a `Remark` is a field that is similar to `Address`, we can reuse a significant bit of code. A copy-paste and search-replace later, you should have something like [this](https://github.com/se-edu/addressbook-level3/commit/4516e099699baa9e2d51801bd26f016d812dedcc#diff-41bb13c581e280c686198251ad6cc337cd5e27032772f06ed9bf7f1440995ece). Note how `Remark` has no constrains and thus does not require input validation. @@ -240,9 +240,9 @@ Let’s change `RemarkCommand` and `RemarkCommandParser` to use the new `Remark` ## Add a placeholder element for remark to the UI -Without getting too deep into `fxml`, let’s go on a 5 minute adventure to get some placeholder text to show up for each person. +Without getting too deep into `fxml`, let’s go on a 5 minute adventure to get some placeholder text to show up for each applicant. -Simply add the following to [`seedu.address.ui.PersonCard`](https://github.com/se-edu/addressbook-level3/commit/850b78879582f38accb05dd20c245963c65ea599#diff-639834f1e05afe2276a86372adf0fe5f69314642c2d93cfa543d614ce5a76688). +Simply add the following to [`seedu.address.ui.ApplicantCard`](https://github.com/se-edu/addressbook-level3/commit/850b78879582f38accb05dd20c245963c65ea599#diff-639834f1e05afe2276a86372adf0fe5f69314642c2d93cfa543d614ce5a76688). **`PersonCard.java`:** @@ -311,9 +311,9 @@ Just add [this one line of code!](https://github.com/se-edu/addressbook-level3/c **`PersonCard.java`:** ``` java -public PersonCard(Person person, int displayedIndex) { +public PersonCard(Person applicant, int displayedIndex) { //... - remark.setText(person.getRemark().value); + remark.setText(applicant.getRemark().value); } ``` @@ -343,25 +343,25 @@ save it with `Model#setPerson()`. throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = new Person( - personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), - personToEdit.getAddress(), remark, personToEdit.getTags()); + Person applicantToEdit = lastShownList.get(index.getZeroBased()); + Person editedApplicant = new Person( + applicantToEdit.getName(), applicantToEdit.getPhone(), applicantToEdit.getEmail(), + applicantToEdit.getAddress(), remark, applicantToEdit.getTags()); - model.setPerson(personToEdit, editedPerson); + model.setPerson(applicantToEdit, editedApplicant); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(generateSuccessMessage(editedPerson)); + return new CommandResult(generateSuccessMessage(editedApplicant)); } /** * Generates a command execution success message based on whether * the remark is added to or removed from - * {@code personToEdit}. + * {@code applicantToEdit}. */ - private String generateSuccessMessage(Person personToEdit) { + private String generateSuccessMessage(Person applicantToEdit) { String message = !remark.value.isEmpty() ? MESSAGE_ADD_REMARK_SUCCESS : MESSAGE_DELETE_REMARK_SUCCESS; - return String.format(message, personToEdit); + return String.format(message, applicantToEdit); } ``` diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md index f29169bc924..be65966f208 100644 --- a/docs/tutorials/RemovingFields.md +++ b/docs/tutorials/RemovingFields.md @@ -28,7 +28,7 @@ IntelliJ IDEA provides a refactoring tool that can identify *most* parts of a re ### Assisted refactoring -The `address` field in `Person` is actually an instance of the `seedu.address.model.person.Address` class. Since removing the `Address` class will break the application, we start by identifying `Address`'s usages. This allows us to see code that depends on `Address` to function properly and edit them on a case-by-case basis. Right-click the `Address` class and select `Refactor` \> `Safe Delete` through the menu. +The `address` field in `Person` is actually an instance of the `seedu.address.model.applicant.Address` class. Since removing the `Address` class will break the application, we start by identifying `Address`'s usages. This allows us to see code that depends on `Address` to function properly and edit them on a case-by-case basis. Right-click the `Address` class and select `Refactor` \> `Safe Delete` through the menu. * :bulb: To make things simpler, you can unselect the options `Search in comments and strings` and `Search for text occurrences` ![Usages detected](../images/remove/UnsafeDelete.png) @@ -100,7 +100,7 @@ In `src/test/data/`, data meant for testing purposes are stored. While keeping t ```json { - "persons": [ { + "applicants": [ { "name": "Person with invalid name field: Ha!ns Mu@ster", "phone": "9482424", "email": "hans@example.com", diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md index 4fb62a83ef6..4598a65a356 100644 --- a/docs/tutorials/TracingCode.md +++ b/docs/tutorials/TracingCode.md @@ -189,22 +189,22 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ @Override public CommandResult execute(Model model) throws CommandException { ... - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + Person applicantToEdit = lastShownList.get(index.getZeroBased()); + Person editedApplicant = createEditedPerson(applicantToEdit, editApplicantDescriptor); + if (!applicantToEdit.isSamePerson(editedApplicant) && model.hasPerson(editedApplicant)) { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } - model.setPerson(personToEdit, editedPerson); + model.setPerson(applicantToEdit, editedApplicant); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedApplicant)); } ``` 1. As suspected, `command#execute()` does indeed make changes to the `model` object. Specifically, - * it uses the `setPerson()` method (defined in the interface `Model` and implemented in `ModelManager` as per the usual pattern) to update the person data. - * it uses the `updateFilteredPersonList` method to ask the `Model` to populate the 'filtered list' with _all_ persons.
- FYI, The 'filtered list' is the list of persons resulting from the most recent operation that will be shown to the user immediately after. For the `edit` command, we populate it with all the persons so that the user can see the edited person along with all other persons. If this was a `find` command, we would be setting that list to contain the search results instead.
- To provide some context, given below is the class diagram of the `Model` component. See if you can figure out where the 'filtered list' of persons is being tracked. + * it uses the `setPerson()` method (defined in the interface `Model` and implemented in `ModelManager` as per the usual pattern) to update the applicant data. + * it uses the `updateFilteredPersonList` method to ask the `Model` to populate the 'filtered list' with _all_ applicants.
+ FYI, The 'filtered list' is the list of applicants resulting from the most recent operation that will be shown to the user immediately after. For the `edit` command, we populate it with all the applicants so that the user can see the edited applicant along with all other applicants. If this was a `find` command, we would be setting that list to contain the search results instead.
+ To provide some context, given below is the class diagram of the `Model` component. See if you can figure out where the 'filtered list' of applicants is being tracked.
* :bulb: This may be a good time to read through the [`Model` component section of the DG](../DeveloperGuide.html#model-component) @@ -231,7 +231,7 @@ Recall from the User Guide that the `edit` command has the format: `edit INDEX [ * {@code JsonSerializableAddressBook}. */ public JsonSerializableAddressBook(ReadOnlyAddressBook source) { - persons.addAll( + applicants.addAll( source.getPersonList() .stream() .map(JsonAdaptedPerson::new) diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 4133aaa0151..f09ad3e847c 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -15,15 +15,15 @@ import seedu.address.commons.util.StringUtil; import seedu.address.logic.Logic; import seedu.address.logic.LogicManager; -import seedu.address.model.AddressBook; +import seedu.address.model.HireLah; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyHireLah; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; import seedu.address.model.util.SampleDataUtil; -import seedu.address.storage.AddressBookStorage; -import seedu.address.storage.JsonAddressBookStorage; +import seedu.address.storage.HireLahStorage; +import seedu.address.storage.JsonHireLahStorage; import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; @@ -48,7 +48,7 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing HireLah ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -56,8 +56,8 @@ public void init() throws Exception { UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); UserPrefs userPrefs = initPrefs(userPrefsStorage); - AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); + HireLahStorage hireLahStorage = new JsonHireLahStorage(userPrefs.getHireLahFilePath()); + storage = new StorageManager(hireLahStorage, userPrefsStorage); initLogging(config); @@ -74,20 +74,20 @@ public void init() throws Exception { * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. */ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { - Optional addressBookOptional; - ReadOnlyAddressBook initialData; + Optional addressBookOptional; + ReadOnlyHireLah initialData; try { - addressBookOptional = storage.readAddressBook(); + addressBookOptional = storage.readHireLah(); if (!addressBookOptional.isPresent()) { - logger.info("Data file not found. Will be starting with a sample AddressBook"); + logger.info("Data file not found. Will be starting with a sample HireLah"); } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleHireLah); } catch (DataConversionException e) { - logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Data file not in the correct format. Will be starting with an empty HireLah"); + initialData = new HireLah(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Problem while reading from the file. Will be starting with an empty HireLah"); + initialData = new HireLah(); } return new ModelManager(initialData, userPrefs); @@ -151,7 +151,7 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { + "Using default user prefs"); initializedPrefs = new UserPrefs(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); + logger.warning("Problem while reading from the file. Will be starting with an empty HireLah"); initializedPrefs = new UserPrefs(); } @@ -167,13 +167,13 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting HireLah " + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping HireLah ] ============================="); try { storage.saveUserPrefs(model.getUserPrefs()); } catch (IOException e) { diff --git a/src/main/java/seedu/address/commons/core/DataType.java b/src/main/java/seedu/address/commons/core/DataType.java new file mode 100644 index 00000000000..c59167230a0 --- /dev/null +++ b/src/main/java/seedu/address/commons/core/DataType.java @@ -0,0 +1,7 @@ +package seedu.address.commons.core; + +public enum DataType { + APPLICANT, + POSITION, + INTERVIEW; +} diff --git a/src/main/java/seedu/address/commons/core/DataTypeFlags.java b/src/main/java/seedu/address/commons/core/DataTypeFlags.java new file mode 100644 index 00000000000..06bd1928215 --- /dev/null +++ b/src/main/java/seedu/address/commons/core/DataTypeFlags.java @@ -0,0 +1,10 @@ +package seedu.address.commons.core; + +/** + * Container for the flags of different types. + */ +public class DataTypeFlags { + public static final char FLAG_APPLICANT = 'a'; + public static final char FLAG_INTERVIEW = 'i'; + public static final char FLAG_POSITION = 'p'; +} diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/address/commons/core/LogsCenter.java index 431e7185e76..34bb1c18116 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/address/commons/core/LogsCenter.java @@ -18,7 +18,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 = "hirelah.log"; private static Level currentLogLevel = Level.INFO; private static final Logger logger = LogsCenter.getLogger(LogsCenter.class); private static FileHandler fileHandler; diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e469..bd09bc60f0a 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -7,7 +7,17 @@ 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_DATETIME = "Date time format should be yyyy-MM-dd HH:mm"; + public static final String MESSAGE_INVALID_DATE = "Date format should be yyyy-MM-dd"; + public static final String MESSAGE_INVALID_APPLICANT_DISPLAYED_INDEX = "The applicant index provided is invalid"; + public static final String MESSAGE_INVALID_INTERVIEW_DISPLAYED_INDEX = "The interview index provided is invalid"; + public static final String MESSAGE_INVALID_POSITION_DISPLAYED_INDEX = "The position index provided is invalid"; + public static final String MESSAGE_INVALID_FLAG = "Flag is invalid!"; + public static final String MESSAGE_NO_FLAG = "No flag is found!"; + public static final String MESSAGE_APPLICANTS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_DUPLICATE_INTERVIEW = "This interview already exists in the address book"; + public static final String MESSAGE_CONFLICTING_INTERVIEW = "This interview would cause a conflict of timings with" + + " a current interview in the address book. Interviews must be " + + "at least 1 hour apart for the same applicant."; + public static final String MESSAGE_APPLICANT_SAME_POSITION = "%1$s already has an interview for %2$s"; } diff --git a/src/main/java/seedu/address/commons/exceptions/ExportCsvOpenException.java b/src/main/java/seedu/address/commons/exceptions/ExportCsvOpenException.java new file mode 100644 index 00000000000..065f303ee10 --- /dev/null +++ b/src/main/java/seedu/address/commons/exceptions/ExportCsvOpenException.java @@ -0,0 +1,15 @@ +package seedu.address.commons.exceptions; + +/** + * Represents an error encountered by export csv. + */ +public class ExportCsvOpenException extends IllegalValueException { + + public ExportCsvOpenException(String message) { + super(message); + } + + public ExportCsvOpenException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/seedu/address/logic/FilterArgument.java b/src/main/java/seedu/address/logic/FilterArgument.java new file mode 100644 index 00000000000..52d95829e91 --- /dev/null +++ b/src/main/java/seedu/address/logic/FilterArgument.java @@ -0,0 +1,39 @@ +package seedu.address.logic; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +public class FilterArgument { + + public static final String MESSAGE_CONSTRAINTS = "Filter arguments should not be blank"; + + public final String argument; + + /** + * Constructs a {@code FilterArgument}. + * + * @param argument A valid argument. + */ + public FilterArgument(String argument) { + requireNonNull(argument); + checkArgument(!argument.isEmpty(), MESSAGE_CONSTRAINTS); + this.argument = argument; + } + + @Override + public String toString() { + return argument; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FilterArgument // instanceof handles nulls + && argument.equals(((FilterArgument) other).argument)); // state check + } + + @Override + public int hashCode() { + return argument.hashCode(); + } +} diff --git a/src/main/java/seedu/address/logic/FilterType.java b/src/main/java/seedu/address/logic/FilterType.java new file mode 100644 index 00000000000..fe6061f45eb --- /dev/null +++ b/src/main/java/seedu/address/logic/FilterType.java @@ -0,0 +1,81 @@ +package seedu.address.logic; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import seedu.address.commons.core.DataType; + +public class FilterType { + + public static final String MESSAGE_CONSTRAINTS = "Invalid filter type for the data type specified\n" + + "Refer to help for the list of filter types"; + + public static final Map> VALID_FILTER_TYPES = loadFilterTypes(); + + public final String type; + + /** + * Constructs a {@code FilterType}. + */ + public FilterType(DataType dataType, String type) { + requireNonNull(dataType, type); + checkArgument(isValidFilterType(dataType, type), MESSAGE_CONSTRAINTS); + this.type = type; + } + + /** + * Returns a map of data type to a valid filter type. + */ + public static Map> loadFilterTypes() { + HashSet applicantTypes = new HashSet<>(); + applicantTypes.add("name"); + applicantTypes.add("gender"); + applicantTypes.add("status"); + applicantTypes.add("tag"); + + HashSet positionTypes = new HashSet<>(); + positionTypes.add("name"); + positionTypes.add("req"); + + HashSet interviewTypes = new HashSet<>(); + interviewTypes.add("appl"); + interviewTypes.add("pos"); + interviewTypes.add("date"); + interviewTypes.add("status"); + + HashMap> filterTypes = new HashMap<>(); + filterTypes.put(DataType.APPLICANT, applicantTypes); + filterTypes.put(DataType.POSITION, positionTypes); + filterTypes.put(DataType.INTERVIEW, interviewTypes); + + return filterTypes; + } + + /** + * Returns true if a given string is a valid filter type for the given data type. + */ + public static boolean isValidFilterType(DataType dataType, String filterType) { + return VALID_FILTER_TYPES.get(dataType).contains(filterType); + } + + @Override + public String toString() { + return type; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FilterType // instanceof handles nulls + && type.equals(((FilterType) other).type)); // state check + } + + @Override + public int hashCode() { + return type.hashCode(); + } +} diff --git a/src/main/java/seedu/address/logic/HelpArgument.java b/src/main/java/seedu/address/logic/HelpArgument.java new file mode 100644 index 00000000000..68a8c40b990 --- /dev/null +++ b/src/main/java/seedu/address/logic/HelpArgument.java @@ -0,0 +1,263 @@ +package seedu.address.logic; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.util.HashMap; + +public class HelpArgument { + public static final String OVERALL_HELPING_DESCRIPTION = + "Looks like you forget something. Don't worry, here are the overall command list. " + + "For a more detail of each command, you can type 'help COMMAND' to view the full description. \n" + + "1. add: Add different types of data into HireLah. \n" + + "2. edit: Edit different types of data in HireLah. \n" + + "3. delete: Delete different types of data in HireLah. \n" + + "4. list: List different data types in HireLah. Can also display filter and sort result. \n" + + "5. pass: Mark an interview as passed. \n" + + "6. fail: Mark an interview as failed. \n" + + "7. accept: Accept a passed interview. \n" + + "8. reject: Reject a passed interview. \n" + + "9. export: Export data to a CSV file. \n" + + "10. clear: Clears all data in HireLah. \n" + + "11. exit: Exits the program"; + + public static final String ADD_COMMAND_DESCRIPTION = + "1. Adding a new applicant: \n" + + "Format: 'add -a n/APPLICANT_NAME ag/AGE g/GENDER p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…' \n" + + "Some notice: \n" + + "- Age provided must be two digits, and cannot start with a 0. eg: “23”. \n" + + "- Name, Phone number and email must be unique \n" + + "- Gender must be M/F. \n" + + "Examples: \n" + + "- add -a n/Benedict ag/20 g/M p/98123456 e/ben@gmail.com a/12 Kent Ridge Drive, 119243 \n" + + "- add -a n/Max ag/15 g/M p/97123456 e/max@yahoo.com a/12 Kent Ridge Drive, 119243 t/Data Analyst \n" + + "\n 2. Adding Interview: \n" + + "Format: 'add -i APPLICANT_INDEX d/DATE p/POSITION_INDEX' \n" + + "Some notice: \n" + + "- Date provided must be in format YYYY-MM-DD HH:MM. \n" + + "- The index refers to the index number shown in the last displayed Applicant" + + " list and Position list. \n" + + "Examples: \n" + + "add -i 1 d/2022-01-01 14:00 p/2 \n \n" + + "3. Adding positions: \n" + + "Format: 'add -p p/POSITION_NAME o/NUM_OPENINGS d/DESCRIPTION [r/REQUIREMENTS]' \n" + + "Some notice: \n" + + "- Positions must have a unique name. \n" + + "- Name provided is case-insensitive. \n" + + "- Number of openings in the position must be between 1 to 5 digits, and cannot start with 0. \n" + + "Examples: \n" + + "add -p p/Senior Software Engineer o/3 d/More than 5 years experience r/JavaScript r/HTML r/CSS"; + + public static final String EDIT_COMMAND_DESCRIPTION = + "1. Editing an applicant: \n" + + "Format: 'edit -a APPLICANT_INDEX [n/APPLICANT_NAME] [ag/AGE] [g/GENDER]" + + " [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…' \n" + + "Some notice: \n" + + "- Edits the Applicant at the specified INDEX. The index refers to the index number shown in the " + + "displayed Applicant list.\n" + + "- At least one of the optional fields must be provided. \n" + + "- Existing values will be updated to the input values. \n" + + "- You can remove all the Applicant’s tags by typing t/ without specifying any tags after it." + + "Examples: \n" + + "edit -a 2 e/belle@yahoo.com a/13 Computing Drive 612345 t/\n" + + "Edits the name, DOB, gender and phone number of the 1st applicant to be Belle, 1960-03-04, " + + "F and 81234567 respectively. \n" + + "\n 2. Editing an Interview: \n" + + "Format: 'edit -i INTERVIEW_INDEX [a/APPLICANT_INDEX] [d/DATE] [p/POSITION_INDEX]' \n" + + "Some notice: \n" + + "- Edits the interview at the specified INTERVIEW_INDEX. The interview index refers to the index number" + + " shown in the last displayed interview list. \n" + + "- At least one optional field must be provided. \n" + + "- Existing attribute of the interview will be updated to the input value. \n" + + "Examples: \n" + + "edit -i 3 a/1 d/2022-01-01 15:00 p/1 \n \n" + + "3. Editing a positions: \n" + + "Format: 'edit -p POSITION_INDEX [p/POSITION_NAME] [o/NUM_OPENINGS] [d/DESCRIPTION] [r/REQUIREMENTS]' \n" + + "Some notice: \n" + + "- Edits the available position with POSITION_INDEX. \n" + + "- At least one optional field must be provided. \n" + + "- Existing attributes of the position will be updated to the input value. \n" + + "- Number of openings in the position cannot be edited to be lower than the current number " + + "of outstanding offers. \n" + + "- Requirements can be removed by providing an empty requirement field. i.e. r/ \n" + + "Examples: \n" + + "edit -p 1 p/Senior Frontend Software Engineer o/5"; + + public static final String DELETE_COMMAND_DESCRIPTION = + "1. Deleting an applicant: \n" + + "Format: 'delete -a APPLICANT_INDEX' \n" + + "Some notice: \n" + + "- Interviews that contain said applicant are also deleted. \n" + + "- Offers for Positions handed out to said applicant will also be removed. \n" + + "- The index refers to the index number shown in the displayed Applicant list. \n" + + "Examples: \n" + + "'list -a f/name a/Betsy' followed by 'delete -a 1' deletes the 1st person name Betsy" + + " in the results of the 'list -a f/name a/Betsy' command.\n" + + "\n 2. Deleting an Interview: \n" + + "Format: 'delete -i INTERVIEW_INDEX' \n" + + "Some notice: \n" + + "- Offer for Positions handed out via the interview will also be removed. \n" + + "- The index refers to the index number shown in the displayed Interview list. \n" + + "Examples: \n" + + "delete -i 3 \n \n" + + "3. Deleting positions: \n" + + "Format: 'delete -p POSITION_INDEX' \n" + + "Some notice: \n" + + "- Interviews that contain said position are also deleted. \n" + + "- However, Applicants that have already accepted a job at said Position," + + " will retain their status as being hired for that Position. \n" + + "- The index refers to the index number shown in the displayed Interview list \n" + + "Examples: \n" + + "delete -p 3"; + + public static final String LIST_COMMAND_DESCRIPTION = + "General command to list all different data type in HireLah " + + "(User can provide optional parameters to filter and sort the data to display). \n" + + "Format: 'list -TYPE [f/FILTER_TYPE a/FILTER_ARGUMENT] [s/SORT_ARGUMENT]' \n" + + "Some notice: \n" + + "- Both 'FILTER_TYPE' and 'FILTER_ARGUMENT' must be provided if you want to use filter. \n" + + "- 'SORT_ARGUMENT' can only be 'asc' (ascending) or 'dsc' (descending) \n" + + "1. List all applicants: \n" + + "Format: 'list -a [f/FILTER_TYPE a/FILTER_ARGUMENT] [s/SORT_ARGUMENT]' \n" + + " + FILTER_TYPE: name, FILTER_ARGUMENT: name keyword: View applicants whose name contains the keyword \n" + + " + FILTER_TYPE: gender, FILTER_ARGUMENT: M/F:" + + " View applicants of the given gender \n" + + " + FILTER_TYPE: status, FILTER_ARGUMENT: available/hired: View applicants with the status given \n" + + " + FILTER_TYPE: tag, FILTER_ARGUMENT: tag keyword: " + + "View applicants with a tag that matches the keywords(s) \n" + + "The applicants displayed can be sorted by their name using the parameter s/SORT_ARGUMENT \n" + + "Examples: \n" + + "- list -a f/status a/hired s/dsc \n" + + "- list -a s/asc \n" + + "\n 2. Listing all Interview for a candidate: \n" + + "Format: 'list -i [f/FILTER_TYPE a/FILTER_ARGUMENT] [s/SORT_ARGUMENT]' \n" + + " + FILTER_TYPE: appl, FILTER_ARGUMENT: applicant name keyword: " + + "View interviews for applicants whose name contains the keyword(s) \n" + + " + FILTER_TYPE: pos, FILTER_ARGUMENT: position name keyword:" + + " View interviews for positions with names that contains the keyword(s) \n" + + " + FILTER_TYPE: date, FILTER_ARGUMENT: Date the interview is happening in 'yyyy-mm-dd':" + + " View interviews which happens on the date provided \n" + + " + FILTER_TYPE: status, FILTER_ARGUMENT: pending / passed / failed / accepted / rejected: " + + "View interviews with the status given \n" + + "The interviews displayed can be sorted by their date using the parameter s/SORT_ARGUMENT \n" + + "Examples: \n" + + "- list -i f/status a/accepted s/asc \n" + + "- list -i f/date a/2022-05-04\n \n" + + "3. Listing all positions: \n" + + "Format: 'list -p [f/FILTER_TYPE a/FILTER_ARGUMENT] [s/SORT_ARGUMENT]' \n" + + " + FILTER_TYPE: name, FILTER_ARGUMENT: name keyword: " + + "View positions with names that contains the keyword(s) \n" + + " + FILTER_TYPE: req, FILTER_ARGUMENT: requirement keyword: " + + "View positions with a requirement that contains the keywords(s) \n" + + "The positions displayed can be sorted by their position name using the parameter s/SORT_ARGUMENT \n" + + "Examples: \n" + + "- list -p f/name a/Software Engineer \n" + + "- list -p f/req a/Java s/dsc"; + + public static final String PASS_COMMAND_DESCRIPTION = + "Passes an existing interview in Hirelah.\nFormat: pass INTERVIEW_INDEX\n" + + "- Passes the Interview at the specified INTERVIEW_INDEX.\n" + + "- Interview must have status pending before it can be passed.\n" + + "- The index must be a positive integer 1, 2, 3, … \n" + + "Additional details:\n" + + "- A job offer is handed out for the interviewed position when applicant passes interview.\n" + + "- Job offer is tracked by the Position interviewed for.\n" + + "- Job can only be offered if 'offered' is less than 'openings'.\n" + + "- A job offered will increase offered by 1.\n" + + "Example: pass 1"; + + public static final String FAIL_COMMAND_DESCRIPTION = + "Fails an existing interview in Hirelah.\nFormat: fail INTERVIEW_INDEX\n" + + "- Passes the Interview at the specified INTERVIEW_INDEX.\n" + + "- Interview must have status pending before it can be failed.\n" + + "- The index must be a positive integer 1, 2, 3, … \n" + + "Example: fail 1"; + + public static final String ACCEPT_COMMAND_DESCRIPTION = + "Accepts an existing passed interview in Hirelah. This command accepts the passed interview, meaning" + + "that the candidate has accepted the job.\nFormat: accept INTERVIEW_INDEX\n" + + "- Accepts the Interview at the specified INTERVIEW_INDEX.\n" + + "- Interview must have status passed before it can be accepted.\n" + + "- The index must be a positive integer 1, 2, 3, …"; + + public static final String REJECT_COMMAND_DESCRIPTION = + "Rejects an existing interview in Hirelah. This command rejects the passed interview, meaning that the " + + "candidate has rejected the job.\nFormat: reject INTERVIEW_INDEX\n" + + "- Reject the Interview at the specified INTERVIEW_INDEX.\n" + + "- Interview must have status passed before it can be accepted.\n" + + "- The index must be a positive integer 1, 2, 3, … \n" + + "Additional details:\n" + + "- Rejecting a job offer will decrement the number of offered in Position"; + + public static final String EXPORT_COMMAND_DESCRIPTION = + "Exports all current displayed data of the specified typo in HireLah to a CSV file." + + " The export csv file will be stored at export_csv folder. \n" + + "Format: export -TYPE\n" + + "TYPE can be a for applicants, p for positions, and i for interviews. \n" + + "Examples:\n" + + "- 'export -p' will export all positions to the corresponding csv file.\n" + + "- 'list -a f/name a/Betsy' then 'export -a' will export csv all applicants" + + " name Betsy to the corresponding csv file."; + + public static final String CLEAR_COMMAND_DESCRIPTION = + "Clears all data in HireLah, including all applicants, positions, and interviews`. \n" + + "Warning: This command cannot be undone!"; + + public static final String EXIT_COMMAND_DESCRIPTION = + "Well its an exit command, of course it going to terminate the program \n" + + "Format: 'exit'"; + + public static final String COMMAND_NOT_FOUND_DESCRIPTION = + "Sorry, we don't have this command. Please try again."; + + public static final String HELP_COMMAND_DESCRIPTION = + "Well what do you expect? New version of help? Here you are, welcome to our new help." + + " Now please type 'help' to get the real help."; + + public static final HashMap HELP_ARGUMENT_WITH_DESCRIPTION = loadHelpArgument(); + + private final String argument; + + /** + * Constructs a {@code HelpArgument}. + */ + public HelpArgument(String argument) { + requireNonNull(argument); + checkArgument(isValidHelpArgument(argument), COMMAND_NOT_FOUND_DESCRIPTION); + this.argument = HELP_ARGUMENT_WITH_DESCRIPTION.get(argument); + } + + private static HashMap loadHelpArgument() { + HashMap argumentWithDescription = new HashMap<>(); + argumentWithDescription.put("add", ADD_COMMAND_DESCRIPTION); + argumentWithDescription.put("edit", EDIT_COMMAND_DESCRIPTION); + argumentWithDescription.put("delete", DELETE_COMMAND_DESCRIPTION); + argumentWithDescription.put("list", LIST_COMMAND_DESCRIPTION); + argumentWithDescription.put("pass", PASS_COMMAND_DESCRIPTION); + argumentWithDescription.put("fail", FAIL_COMMAND_DESCRIPTION); + argumentWithDescription.put("accept", ACCEPT_COMMAND_DESCRIPTION); + argumentWithDescription.put("reject", REJECT_COMMAND_DESCRIPTION); + argumentWithDescription.put("export", EXPORT_COMMAND_DESCRIPTION); + argumentWithDescription.put("exit", EXIT_COMMAND_DESCRIPTION); + argumentWithDescription.put("help", HELP_COMMAND_DESCRIPTION); + argumentWithDescription.put("clear", CLEAR_COMMAND_DESCRIPTION); + argumentWithDescription.put("", OVERALL_HELPING_DESCRIPTION); + return argumentWithDescription; + } + + public static Boolean isValidHelpArgument(String argument) { + return HELP_ARGUMENT_WITH_DESCRIPTION.containsKey(argument); + } + + @Override + public String toString() { + return this.argument; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof HelpArgument // instanceof handles nulls + && argument.equals(((HelpArgument) other).argument)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..464c41b6851 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -1,14 +1,18 @@ package seedu.address.logic; +import java.io.FileNotFoundException; import java.nio.file.Path; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.commons.exceptions.ExportCsvOpenException; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; +import seedu.address.model.ReadOnlyHireLah; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; /** * API of the Logic component @@ -21,22 +25,29 @@ public interface Logic { * @throws CommandException If an error occurs during command execution. * @throws ParseException If an error occurs during parsing. */ - CommandResult execute(String commandText) throws CommandException, ParseException; + CommandResult execute(String commandText) throws CommandException, FileNotFoundException, ParseException, + ExportCsvOpenException; /** - * Returns the AddressBook. + * Returns the HireLah. * - * @see seedu.address.model.Model#getAddressBook() + * @see seedu.address.model.Model#getHireLah() */ - ReadOnlyAddressBook getAddressBook(); + ReadOnlyHireLah getHireLah(); - /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of applicants */ + ObservableList getFilteredApplicantList(); + + /** Returns an unmodifiable view of the filtered list of positions */ + ObservableList getFilteredPositionList(); + + /** Returns an unmodifiable view of the filtered list of interviews */ + ObservableList getFilteredInterviewList(); /** - * Returns the user prefs' address book file path. + * Returns the user prefs' HireLah file path. */ - Path getAddressBookFilePath(); + Path getHireLahFilePath(); /** * Returns the user prefs' GUI settings. diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9d9c6d15bdc..d55d8cfa789 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -1,5 +1,6 @@ package seedu.address.logic; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Path; import java.util.logging.Logger; @@ -7,14 +8,17 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.ExportCsvOpenException; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; +import seedu.address.logic.parser.HireLahParser; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; +import seedu.address.model.ReadOnlyHireLah; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; import seedu.address.storage.Storage; /** @@ -26,7 +30,7 @@ public class LogicManager implements Logic { private final Model model; private final Storage storage; - private final AddressBookParser addressBookParser; + private final HireLahParser hireLahParser; /** * Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}. @@ -34,19 +38,20 @@ public class LogicManager implements Logic { public LogicManager(Model model, Storage storage) { this.model = model; this.storage = storage; - addressBookParser = new AddressBookParser(); + hireLahParser = new HireLahParser(); } @Override - public CommandResult execute(String commandText) throws CommandException, ParseException { + public CommandResult execute(String commandText) throws CommandException, FileNotFoundException, ParseException, + ExportCsvOpenException { logger.info("----------------[USER COMMAND][" + commandText + "]"); CommandResult commandResult; - Command command = addressBookParser.parseCommand(commandText); + Command command = hireLahParser.parseCommand(commandText); commandResult = command.execute(model); try { - storage.saveAddressBook(model.getAddressBook()); + storage.saveHireLah(model.getHireLah()); } catch (IOException ioe) { throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); } @@ -55,18 +60,28 @@ public CommandResult execute(String commandText) throws CommandException, ParseE } @Override - public ReadOnlyAddressBook getAddressBook() { - return model.getAddressBook(); + public ReadOnlyHireLah getHireLah() { + return model.getHireLah(); } @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); + public ObservableList getFilteredApplicantList() { + return model.getFilteredApplicantList(); } @Override - public Path getAddressBookFilePath() { - return model.getAddressBookFilePath(); + public ObservableList getFilteredPositionList() { + return model.getFilteredPositionList(); + } + + @Override + public ObservableList getFilteredInterviewList() { + return model.getFilteredInterviewList(); + } + + @Override + public Path getHireLahFilePath() { + return model.getHireLahFilePath(); } @Override diff --git a/src/main/java/seedu/address/logic/SortArgument.java b/src/main/java/seedu/address/logic/SortArgument.java new file mode 100644 index 00000000000..fb57226b9dc --- /dev/null +++ b/src/main/java/seedu/address/logic/SortArgument.java @@ -0,0 +1,48 @@ +package seedu.address.logic; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +public class SortArgument { + + public static final String VALIDATION_REGEX = "asc|dsc"; + + public static final String MESSAGE_CONSTRAINTS = "Invalid sort argument! Sort argument is either asc or dsc"; + + public final String argument; + + /** + * Constructs a {@code SortArgument}. + * + * @param argument A valid argument. + */ + public SortArgument(String argument) { + requireNonNull(argument); + checkArgument(isValidSortArgument(argument), MESSAGE_CONSTRAINTS); + this.argument = argument; + } + + /** + * Tests is a given string is a valid {@code SortArgument} according to the regular expression. + */ + public static boolean isValidSortArgument(String test) { + return test.toLowerCase().matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return argument; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SortArgument // instanceof handles nulls + && argument.equals(((SortArgument) other).argument)); // state check + } + + @Override + public int hashCode() { + return argument.hashCode(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 71656d7c5c8..a0da7922ce3 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -1,67 +1,14 @@ package seedu.address.logic.commands; -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; -import seedu.address.model.person.Person; /** - * Adds a person to the address book. + * Adds the specified data to the application. */ -public class AddCommand extends Command { - +public abstract class AddCommand extends Command { public static final String COMMAND_WORD = "add"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; - - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; - - private final Person toAdd; - - /** - * Creates an AddCommand to add the specified {@code Person} - */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); - } - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddCommand // instanceof handles nulls - && toAdd.equals(((AddCommand) other).toAdd)); - } + public abstract CommandResult execute(Model model) throws CommandException; } diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..ad677174f67 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -2,22 +2,28 @@ import static java.util.Objects.requireNonNull; -import seedu.address.model.AddressBook; +import seedu.address.commons.core.DataType; +import seedu.address.model.HireLah; import seedu.address.model.Model; /** - * Clears the address book. + * Clears the application. */ 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 = "All data in HireLah has been cleared!"; @Override public CommandResult execute(Model model) { requireNonNull(model); - model.setAddressBook(new AddressBook()); - return new CommandResult(MESSAGE_SUCCESS); + model.setHireLah(new HireLah()); + return new CommandResult(MESSAGE_SUCCESS, getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return null; } } diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java index 64f18992160..4b7da49abdf 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/address/logic/commands/Command.java @@ -1,6 +1,11 @@ package seedu.address.logic.commands; +import java.io.FileNotFoundException; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.exceptions.ExportCsvOpenException; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; /** @@ -15,6 +20,14 @@ public abstract class Command { * @return feedback message of the operation result for display * @throws CommandException If an error occurs during command execution. */ - public abstract CommandResult execute(Model model) throws CommandException; + public abstract CommandResult execute(Model model) throws CommandException, FileNotFoundException, ParseException, + ExportCsvOpenException; + + /** + * Returns the data type associated with the command. + * + * @return DataType of the command + */ + public abstract DataType getCommandDataType(); } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 92f900b7916..c8a917cd4ec 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -4,6 +4,8 @@ import java.util.Objects; +import seedu.address.commons.core.DataType; + /** * Represents the result of a command execution. */ @@ -11,6 +13,9 @@ public class CommandResult { private final String feedbackToUser; + /** The type of data the command is related to. */ + private final DataType dataType; + /** Help information should be shown to the user. */ private final boolean showHelp; @@ -20,7 +25,8 @@ public class CommandResult { /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, DataType dataType, boolean showHelp, boolean exit) { + this.dataType = dataType; this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; @@ -30,14 +36,18 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, * and other fields set to their default value. */ - public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + public CommandResult(String feedbackToUser, DataType dataType) { + this(feedbackToUser, dataType, false, false); } public String getFeedbackToUser() { return feedbackToUser; } + public DataType getDataType() { + return dataType; + } + public boolean isShowHelp() { return showHelp; } @@ -59,13 +69,14 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) + && dataType == otherCommandResult.dataType && showHelp == otherCommandResult.showHelp && exit == otherCommandResult.exit; } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, dataType, showHelp, exit); } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 02fd256acba..265a865329f 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -1,53 +1,14 @@ package seedu.address.logic.commands; -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; -import seedu.address.model.person.Person; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes the specified data from the application. */ -public class DeleteCommand extends Command { - +public abstract class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - - private final Index targetIndex; - - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); - } - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof DeleteCommand // instanceof handles nulls - && targetIndex.equals(((DeleteCommand) other).targetIndex)); // state check - } + public abstract CommandResult execute(Model model) throws CommandException; } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 7e36114902f..450f44e3136 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -1,226 +1,14 @@ package seedu.address.logic.commands; -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; /** - * Edits the details of an existing person in the address book. + * Edits the details of the specified data type in the application. */ -public class EditCommand extends Command { - +public abstract 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. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "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_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - - private final Index index; - private final EditPersonDescriptor editPersonDescriptor; - - /** - * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with - */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { - requireNonNull(index); - requireNonNull(editPersonDescriptor); - - this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); - } - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); - } - - /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. - */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; - - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditCommand)) { - return false; - } - - // state check - EditCommand e = (EditCommand) other; - return index.equals(e.index) - && editPersonDescriptor.equals(e.editPersonDescriptor); - } - - /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. - */ - public static class EditPersonDescriptor { - private Name name; - private Phone phone; - private Email email; - private Address address; - private Set tags; - - public EditPersonDescriptor() {} - - /** - * Copy constructor. - * A defensive copy of {@code tags} is used internally. - */ - public EditPersonDescriptor(EditPersonDescriptor toCopy) { - setName(toCopy.name); - setPhone(toCopy.phone); - setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); - } - - /** - * Returns true if at least one field is edited. - */ - public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); - } - - public void setName(Name name) { - this.name = name; - } - - public Optional getName() { - return Optional.ofNullable(name); - } - - public void setPhone(Phone phone) { - this.phone = phone; - } - - public Optional getPhone() { - return Optional.ofNullable(phone); - } - - public void setEmail(Email email) { - this.email = email; - } - - public Optional getEmail() { - return Optional.ofNullable(email); - } - - public void setAddress(Address address) { - this.address = address; - } - - public Optional
getAddress() { - return Optional.ofNullable(address); - } - - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; - } - - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { - return false; - } - - // state check - EditPersonDescriptor e = (EditPersonDescriptor) other; - - return getName().equals(e.getName()) - && getPhone().equals(e.getPhone()) - && getEmail().equals(e.getEmail()) - && getAddress().equals(e.getAddress()) - && getTags().equals(e.getTags()); - } - } + public abstract CommandResult execute(Model model) throws CommandException; } diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..9943124f503 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -1,5 +1,6 @@ package seedu.address.logic.commands; +import seedu.address.commons.core.DataType; import seedu.address.model.Model; /** @@ -9,11 +10,15 @@ 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 HireLah as requested ..."; @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, getCommandDataType(), false, true); } + @Override + public DataType getCommandDataType() { + return null; + } } diff --git a/src/main/java/seedu/address/logic/commands/ExportCsvCommand.java b/src/main/java/seedu/address/logic/commands/ExportCsvCommand.java new file mode 100644 index 00000000000..94d4c13eb84 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ExportCsvCommand.java @@ -0,0 +1,14 @@ +package seedu.address.logic.commands; + +import java.io.FileNotFoundException; + +import seedu.address.commons.exceptions.ExportCsvOpenException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +public abstract class ExportCsvCommand extends Command { + public static final String COMMAND_WORD = "export"; + + public abstract CommandResult execute(Model model) throws CommandException, FileNotFoundException, + ExportCsvOpenException; +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java deleted file mode 100644 index d6b19b0a0de..00000000000 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ /dev/null @@ -1,42 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.core.Messages; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. - */ -public class FindCommand extends Command { - - public static final String COMMAND_WORD = "find"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; - - private final NameContainsKeywordsPredicate predicate; - - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; - } - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof FindCommand // instanceof handles nulls - && predicate.equals(((FindCommand) other).predicate)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..c253641bdb7 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -1,24 +1,10 @@ package seedu.address.logic.commands; -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - import seedu.address.model.Model; -/** - * Lists all persons in the address book to the user. - */ -public class ListCommand extends Command { - +public abstract class ListCommand extends Command { public static final String COMMAND_WORD = "list"; - public static final String MESSAGE_SUCCESS = "Listed all persons"; - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } + public abstract CommandResult execute(Model model); } diff --git a/src/main/java/seedu/address/logic/commands/applicant/AddApplicantCommand.java b/src/main/java/seedu/address/logic/commands/applicant/AddApplicantCommand.java new file mode 100644 index 00000000000..8fd5b02aff5 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/applicant/AddApplicantCommand.java @@ -0,0 +1,91 @@ +package seedu.address.logic.commands.applicant; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_AGE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GENDER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import seedu.address.commons.core.DataType; +import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.applicant.Applicant; + +/** + * Adds an applicant to the application. + */ +public class AddApplicantCommand extends AddCommand { + + public static final String MESSAGE_USAGE = COMMAND_WORD + " -a: Adds an applicant to HireLah. " + + "Parameters: " + + PREFIX_NAME + "NAME " + + PREFIX_PHONE + "PHONE " + + PREFIX_EMAIL + "EMAIL " + + PREFIX_AGE + "AGE " + + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_GENDER + "GENDER " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " -a " + + PREFIX_NAME + "John Doe " + + PREFIX_PHONE + "98765432 " + + PREFIX_EMAIL + "johnd@example.com " + + PREFIX_AGE + "23 " + + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " + + PREFIX_GENDER + "M " + + PREFIX_TAG + "AWS Certified " + + PREFIX_TAG + "Ex-Facebook"; + + public static final String MESSAGE_SUCCESS = "New applicant added: %1$s"; + public static final String MESSAGE_DUPLICATE_APPLICANT = "This applicant already exists in HireLah application"; + private static final String MESSAGE_DUPLICATE_EMAIL = "The email is already used by %1$s"; + private static final String MESSAGE_DUPLICATE_PHONE = "The phone number is already used by %1$s"; + + private final Applicant toAdd; + + /** + * Creates an AddApplicantCommand to add the specified {@code Applicant} + */ + public AddApplicantCommand(Applicant applicant) { + requireNonNull(applicant); + toAdd = applicant; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasApplicant(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_APPLICANT); + } + + Applicant applicantWithEmail = model.getApplicantWithEmail(toAdd.getEmail()); + if (applicantWithEmail != null) { + throw new CommandException(String.format(MESSAGE_DUPLICATE_EMAIL, applicantWithEmail.getName().fullName)); + } + + Applicant applicantWithPhone = model.getApplicantWithPhone(toAdd.getPhone()); + if (applicantWithPhone != null) { + throw new CommandException(String.format(MESSAGE_DUPLICATE_PHONE, applicantWithPhone.getName().fullName)); + } + + model.addApplicant(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd), getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.APPLICANT; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddApplicantCommand // instanceof handles nulls + && toAdd.equals(((AddApplicantCommand) other).toAdd)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/applicant/DeleteApplicantCommand.java b/src/main/java/seedu/address/logic/commands/applicant/DeleteApplicantCommand.java new file mode 100644 index 00000000000..0550e65b6b2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/applicant/DeleteApplicantCommand.java @@ -0,0 +1,87 @@ +package seedu.address.logic.commands.applicant; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; + +/** + * Deletes an applicant identified using it's displayed index from the application, + * and the interviews associated with the applicant as well. + */ +public class DeleteApplicantCommand extends DeleteCommand { + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " -a : Deletes the applicant identified by the index number used in the displayed applicant list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " -a 1"; + + public static final String MESSAGE_DELETE_APPLICANT_SUCCESS = "Deleted Applicant: %1$s"; + + public static final String MESSAGE_DELETE_INTERVIEWS = "Deleted %d related interview(s)"; + + private final Index targetIndex; + + private final Logger logger = LogsCenter.getLogger(DeleteApplicantCommand.class); + + public DeleteApplicantCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredApplicantList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_APPLICANT_DISPLAYED_INDEX); + } + + Applicant applicantToDelete = lastShownList.get(targetIndex.getZeroBased()); + + ArrayList interviewsToDelete = model.getApplicantsInterviews(applicantToDelete); + for (Interview i : interviewsToDelete) { + model.deleteInterview(i); + + if (i.isPassedStatus()) { + Position oldPosition = i.getPosition(); + Position newPosition = i.getPosition().rejectOffer(); + model.updatePosition(oldPosition, newPosition); + } + + logger.log(Level.INFO, String.format("Deleted interview: %1$s", i)); + } + + model.deleteApplicant(applicantToDelete); + return new CommandResult( + String.format(MESSAGE_DELETE_APPLICANT_SUCCESS, applicantToDelete) + "\n" + + String.format(MESSAGE_DELETE_INTERVIEWS, interviewsToDelete.size()), + getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.APPLICANT; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteApplicantCommand // instanceof handles nulls + && targetIndex.equals(((DeleteApplicantCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/applicant/EditApplicantCommand.java b/src/main/java/seedu/address/logic/commands/applicant/EditApplicantCommand.java new file mode 100644 index 00000000000..1c29eecf17b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/applicant/EditApplicantCommand.java @@ -0,0 +1,298 @@ +package seedu.address.logic.commands.applicant; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.DataTypeFlags.FLAG_APPLICANT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_AGE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GENDER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_APPLICANTS; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.applicant.Address; +import seedu.address.model.applicant.Age; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.applicant.Email; +import seedu.address.model.applicant.Gender; +import seedu.address.model.applicant.HiredStatus; +import seedu.address.model.applicant.Name; +import seedu.address.model.applicant.Phone; +import seedu.address.model.tag.Tag; + +/** + * Edits the details of an existing applicant in the application. + */ +public class EditApplicantCommand extends EditCommand { + + public static final String MESSAGE_USAGE = COMMAND_WORD + " -" + FLAG_APPLICANT + + ": Edits the details of the applicant " + "identified " + + "by the index number used in the displayed applicant list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INTERVIEW_INDEX (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_PHONE + "PHONE] " + + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_AGE + "AGE] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_GENDER + "GENDER] " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " -" + FLAG_APPLICANT + " 1 " + + PREFIX_PHONE + "91234567 " + + PREFIX_EMAIL + "johndoe@example.com"; + + public static final String MESSAGE_EDIT_APPLICANT_SUCCESS = "Edited Applicant: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_APPLICANT = "This applicant already exists in HireLah."; + private static final String MESSAGE_DUPLICATE_EMAIL = "The email is already used by %1$s"; + private static final String MESSAGE_DUPLICATE_PHONE = "The phone number is already used by %1$s"; + + private final Index index; + private final EditApplicantDescriptor editApplicantDescriptor; + + /** + * @param index of the applicant in the filtered applicant list to edit + * @param editApplicantDescriptor details to edit the applicant with + */ + public EditApplicantCommand(Index index, EditApplicantDescriptor editApplicantDescriptor) { + requireNonNull(index); + requireNonNull(editApplicantDescriptor); + + this.index = index; + this.editApplicantDescriptor = new EditApplicantDescriptor(editApplicantDescriptor); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredApplicantList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_APPLICANT_DISPLAYED_INDEX); + } + + Applicant applicantToEdit = lastShownList.get(index.getZeroBased()); + Applicant editedApplicant = createEditedApplicant(applicantToEdit, editApplicantDescriptor); + + if (!applicantToEdit.isSameApplicant(editedApplicant) && model.hasApplicant(editedApplicant)) { + throw new CommandException(MESSAGE_DUPLICATE_APPLICANT); + } + + boolean emailNotEdited = applicantToEdit.getEmail().equals(editedApplicant.getEmail()); + if (!emailNotEdited) { + Applicant applicantWithEmail = model.getApplicantWithEmail(editedApplicant.getEmail()); + if (applicantWithEmail != null) { + throw new CommandException(String.format(MESSAGE_DUPLICATE_EMAIL, + applicantWithEmail.getName().fullName)); + } + } + + boolean phoneNotEdited = applicantToEdit.getPhone().equals(editedApplicant.getPhone()); + if (!phoneNotEdited) { + Applicant applicantWithPhone = model.getApplicantWithPhone(editedApplicant.getPhone()); + if (applicantWithPhone != null) { + throw new CommandException(String.format(MESSAGE_DUPLICATE_PHONE, + applicantWithPhone.getName().fullName)); + } + } + + model.updateApplicant(applicantToEdit, editedApplicant); + model.updateFilteredApplicantList(PREDICATE_SHOW_ALL_APPLICANTS); + return new CommandResult(String.format(MESSAGE_EDIT_APPLICANT_SUCCESS, editedApplicant), getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.APPLICANT; + } + + /** + * Creates and returns a {@code Applicant} with the details of {@code applicantToEdit} + * edited with {@code editApplicantDescriptor}. + */ + private static Applicant createEditedApplicant(Applicant applicantToEdit, + EditApplicantDescriptor editApplicantDescriptor) { + assert applicantToEdit != null; + + Name updatedName = editApplicantDescriptor.getName().orElse(applicantToEdit.getName()); + Phone updatedPhone = editApplicantDescriptor.getPhone().orElse(applicantToEdit.getPhone()); + Email updatedEmail = editApplicantDescriptor.getEmail().orElse(applicantToEdit.getEmail()); + Age updatedAge = editApplicantDescriptor.getAge().orElse(applicantToEdit.getAge()); + Address updatedAddress = editApplicantDescriptor.getAddress().orElse(applicantToEdit.getAddress()); + Gender updatedGender = editApplicantDescriptor.getGender().orElse(applicantToEdit.getGender()); + HiredStatus updatedHiredStatus = editApplicantDescriptor.getStatus().orElse(applicantToEdit.getStatus()); + Set updatedTags = editApplicantDescriptor.getTags().orElse(applicantToEdit.getTags()); + + return new Applicant(updatedName, updatedPhone, updatedEmail, updatedAge, updatedAddress, + updatedGender, updatedHiredStatus, updatedTags); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditApplicantCommand)) { + return false; + } + + // state check + EditApplicantCommand e = (EditApplicantCommand) other; + return index.equals(e.index) + && editApplicantDescriptor.equals(e.editApplicantDescriptor); + } + + /** + * Stores the details to edit the applicant with. Each non-empty field value will replace the + * corresponding field value of the applicant. + */ + public static class EditApplicantDescriptor { + private Name name; + private Phone phone; + private Email email; + private Age age; + private Address address; + private Gender gender; + private HiredStatus hiredStatus; + private Set tags; + + public EditApplicantDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditApplicantDescriptor(EditApplicantDescriptor toCopy) { + setName(toCopy.name); + setPhone(toCopy.phone); + setEmail(toCopy.email); + setAge(toCopy.age); + setAddress(toCopy.address); + setGender(toCopy.gender); + setStatus(toCopy.hiredStatus); + setTags(toCopy.tags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, age, gender, phone, email, address, tags); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setPhone(Phone phone) { + this.phone = phone; + } + + public Optional getPhone() { + return Optional.ofNullable(phone); + } + + public void setEmail(Email email) { + this.email = email; + } + + public Optional getEmail() { + return Optional.ofNullable(email); + } + + public void setAge(Age age) { + this.age = age; + } + + public Optional getAge() { + return Optional.ofNullable(age); + } + + public void setAddress(Address address) { + this.address = address; + } + + public Optional
getAddress() { + return Optional.ofNullable(address); + } + + public void setGender(Gender gender) { + this.gender = gender; + } + + public Optional getGender() { + return Optional.ofNullable(gender); + } + + public void setStatus(HiredStatus hiredStatus) { + this.hiredStatus = hiredStatus; + } + + public Optional getStatus() { + return Optional.ofNullable(hiredStatus); + } + + /** + * Sets {@code tags} to this object's {@code tags}. + * A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + + /** + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Optional> getTags() { + return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditApplicantDescriptor)) { + return false; + } + + // state check + EditApplicantDescriptor e = (EditApplicantDescriptor) other; + + return getName().equals(e.getName()) + && getPhone().equals(e.getPhone()) + && getEmail().equals(e.getEmail()) + && getAddress().equals(e.getAddress()) + && getTags().equals(e.getTags()); + } + + + } +} diff --git a/src/main/java/seedu/address/logic/commands/applicant/ExportApplicantCsvCommand.java b/src/main/java/seedu/address/logic/commands/applicant/ExportApplicantCsvCommand.java new file mode 100644 index 00000000000..3ed0fa7cf1b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/applicant/ExportApplicantCsvCommand.java @@ -0,0 +1,26 @@ +package seedu.address.logic.commands.applicant; +import java.io.FileNotFoundException; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.exceptions.ExportCsvOpenException; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.ExportCsvCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + + +public class ExportApplicantCsvCommand extends ExportCsvCommand { + public static final String MESSAGE_SUCCESS = "Applicant CSV is successfully exported at export_csv/applicant.csv"; + + @Override + public CommandResult execute(Model model) throws CommandException, FileNotFoundException, ExportCsvOpenException { + model.exportCsvApplicant(); + return new CommandResult(MESSAGE_SUCCESS, getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.APPLICANT; + } + +} diff --git a/src/main/java/seedu/address/logic/commands/applicant/ListApplicantCommand.java b/src/main/java/seedu/address/logic/commands/applicant/ListApplicantCommand.java new file mode 100644 index 00000000000..8e6eb039f81 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/applicant/ListApplicantCommand.java @@ -0,0 +1,123 @@ +package seedu.address.logic.commands.applicant; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_ARGUMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_TYPE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SORT_ARGUMENT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_APPLICANTS; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.function.Predicate; + +import seedu.address.commons.core.DataType; +import seedu.address.logic.FilterArgument; +import seedu.address.logic.FilterType; +import seedu.address.logic.SortArgument; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.ListCommand; +import seedu.address.model.Model; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.applicant.ApplicantGenderPredicate; +import seedu.address.model.applicant.ApplicantNameComparator; +import seedu.address.model.applicant.ApplicantNamePredicate; +import seedu.address.model.applicant.ApplicantStatusPredicate; +import seedu.address.model.applicant.ApplicantTagPredicate; + +/** + * Lists applicants in HireLah to the user. + */ +public class ListApplicantCommand extends ListCommand { + + public static final String MESSAGE_USAGE = COMMAND_WORD + " -a: List applicants with optional parameters." + + "\nOptional parameters: " + + PREFIX_FILTER_TYPE + "FILTER_TYPE " + + PREFIX_FILTER_ARGUMENT + "FILTER_TYPE " + + PREFIX_SORT_ARGUMENT + "[asc/dsc] " + + "\nExample: " + COMMAND_WORD + " -a " + + PREFIX_FILTER_TYPE + "gender " + + PREFIX_FILTER_ARGUMENT + "M " + + PREFIX_SORT_ARGUMENT + "dsc "; + + public static final String MESSAGE_SUCCESS = "Listed %1$d applicants"; + + private FilterType filterType; + private FilterArgument filterArgument; + private SortArgument sortArgument; + + /** + * Creates an ListApplicantCommand to display all {@code Applicant} + */ + public ListApplicantCommand() { + filterType = null; + filterArgument = null; + sortArgument = null; + } + + /** + * Creates an ListApplicantCommand to filter and display {@code Applicant} + */ + public ListApplicantCommand(FilterType filterType, FilterArgument filterArgument) { + this.filterType = filterType; + this.filterArgument = filterArgument; + } + + public ListApplicantCommand(SortArgument sortArgument) { + this.sortArgument = sortArgument; + } + + /** + * Creates an ListApplicantCommand to filter and sort then display {@code Applicant} + */ + public ListApplicantCommand(FilterType filterType, FilterArgument filterArgument, SortArgument sortArgument) { + this.filterArgument = filterArgument; + this.filterType = filterType; + this.sortArgument = sortArgument; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + if (filterType != null && filterArgument != null && sortArgument != null) { + Predicate predicate = getFilterPredicate(filterType, filterArgument); + Comparator comparator = new ApplicantNameComparator(sortArgument.toString()); + model.updateFilterAndSortApplicantList(predicate, comparator); + } else if (filterType != null && filterArgument != null) { + Predicate predicate = getFilterPredicate(filterType, filterArgument); + model.updateFilteredApplicantList(predicate); + } else if (sortArgument != null) { + Comparator comparator = new ApplicantNameComparator(sortArgument.toString()); + model.updateSortApplicantList(comparator); + } else { + model.updateFilteredApplicantList(PREDICATE_SHOW_ALL_APPLICANTS); + } + + return new CommandResult( + String.format(MESSAGE_SUCCESS, model.getFilteredApplicantList().size()), getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.APPLICANT; + } + + /** + * Returns the suitable {@code Predicate} based on the given {@code filterType} and {@code filterArgument} + */ + public Predicate getFilterPredicate(FilterType filterType, FilterArgument filterArgument) { + if (filterType.type.equals("name")) { + String[] nameKeywords = filterArgument.toString().split("\\s+"); + return new ApplicantNamePredicate(Arrays.asList(nameKeywords)); + } else if (filterType.type.equals("gender")) { + return new ApplicantGenderPredicate(filterArgument.toString()); + } else if (filterType.type.equals("status")) { + return new ApplicantStatusPredicate(filterArgument.toString()); + } else if (filterType.type.equals("tag")) { + String[] tagKeywords = filterArgument.toString().split("\\s+"); + return new ApplicantTagPredicate(Arrays.asList(tagKeywords)); + } + + assert true : "Filter type should be valid"; + return null; + } +} 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/commands/help/DetailHelpCommand.java b/src/main/java/seedu/address/logic/commands/help/DetailHelpCommand.java new file mode 100644 index 00000000000..973bbb5732c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/help/DetailHelpCommand.java @@ -0,0 +1,37 @@ +package seedu.address.logic.commands.help; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.DataType; +import seedu.address.logic.HelpArgument; +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; + +public class DetailHelpCommand extends HelpCommand { + private HelpArgument helpArgument; + + /** + * Constructor for DetailHelpCommand class + */ + public DetailHelpCommand(HelpArgument helpArgument) { + requireNonNull(helpArgument); + this.helpArgument = helpArgument; + } + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + return new CommandResult(helpArgument.toString(), getCommandDataType(), true, false); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DetailHelpCommand // instanceof handles nulls + && helpArgument.equals(((DetailHelpCommand) other).helpArgument)); // state check + } + + @Override + public DataType getCommandDataType() { + return null; + } +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/help/HelpCommand.java similarity index 62% rename from src/main/java/seedu/address/logic/commands/HelpCommand.java rename to src/main/java/seedu/address/logic/commands/help/HelpCommand.java index bf824f91bd0..0b6c30c564c 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/help/HelpCommand.java @@ -1,11 +1,13 @@ -package seedu.address.logic.commands; +package seedu.address.logic.commands.help; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; import seedu.address.model.Model; /** * Format full help instructions for every command for display. */ -public class HelpCommand extends Command { +public abstract class HelpCommand extends Command { public static final String COMMAND_WORD = "help"; @@ -15,7 +17,5 @@ public class HelpCommand extends Command { public static final String SHOWING_HELP_MESSAGE = "Opened help window."; @Override - public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); - } + public abstract CommandResult execute(Model model); } diff --git a/src/main/java/seedu/address/logic/commands/interview/AcceptInterviewCommand.java b/src/main/java/seedu/address/logic/commands/interview/AcceptInterviewCommand.java new file mode 100644 index 00000000000..bbe70b2d222 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/interview/AcceptInterviewCommand.java @@ -0,0 +1,78 @@ +package seedu.address.logic.commands.interview; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; + +public class AcceptInterviewCommand extends Command { + public static final String COMMAND_WORD = "accept"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_ACCEPT_INTERVIEW_SUCCESS = "Accept Interview: %1$s"; + public static final String MESSAGE_INTERVIEW_CANNOT_BE_ACCEPTED = "Only passed interviews can be accepted!"; + public static final String MESSAGE_APPLICANT_HAS_JOB = "The applicant already has a job, " + + "so they cannot accept a new one."; + + private final Index targetIndex; + + public AcceptInterviewCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredInterviewList(); + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_INTERVIEW_DISPLAYED_INDEX); + } + + Interview interviewToAccept = lastShownList.get(targetIndex.getZeroBased()); + if (!interviewToAccept.isAcceptableInterview()) { + throw new CommandException(MESSAGE_INTERVIEW_CANNOT_BE_ACCEPTED); + } + Applicant oldApplicant = interviewToAccept.getApplicant(); + if (oldApplicant.isHired()) { + throw new CommandException(MESSAGE_APPLICANT_HAS_JOB); + } + Position oldPosition = interviewToAccept.getPosition(); + Position newPosition = interviewToAccept.getPosition().acceptOffer(); + Applicant newApplicant = interviewToAccept.getApplicant().setStatus(oldApplicant, newPosition); + Interview acceptedInterview = new Interview(newApplicant, interviewToAccept.getDate(), + newPosition); + // Interview has default status of "Pending", need to make it passed and then accepted + acceptedInterview.markAsPassed(); + acceptedInterview.markAsAccepted(); + model.setInterview(interviewToAccept, acceptedInterview); + model.updateApplicant(oldApplicant, newApplicant); + model.updatePosition(oldPosition, newPosition); + return new CommandResult(String.format(MESSAGE_ACCEPT_INTERVIEW_SUCCESS, acceptedInterview), + getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.INTERVIEW; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AcceptInterviewCommand // instanceof handles nulls + && targetIndex.equals(((AcceptInterviewCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/interview/AddInterviewCommand.java b/src/main/java/seedu/address/logic/commands/interview/AddInterviewCommand.java new file mode 100644 index 00000000000..87c938b9b9c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/interview/AddInterviewCommand.java @@ -0,0 +1,97 @@ +package seedu.address.logic.commands.interview; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; + +import java.time.LocalDateTime; +import java.util.List; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; + +public class AddInterviewCommand extends AddCommand { + public static final String MESSAGE_USAGE = COMMAND_WORD + " -i: Adds an interview to the HireLah application. " + + "Parameters: APPLICANT_INDEX (must be a positive integer) " + + PREFIX_DATE + "DATE " + + PREFIX_POSITION + "POSITION_INDEX" + "\n" + + "Example: " + COMMAND_WORD + " -i " + "1 " + + PREFIX_DATE + "2022-01-01 12:00 " + + PREFIX_POSITION + "1"; + + public static final String MESSAGE_SUCCESS = "New interview added: %1$s"; + public static final String MESSAGE_POSITION_NO_OPENING = "The position the applicant is interviewing for " + + "has no openings, so an interview cannot be scheduled."; + public static final String MESSAGE_APPLICANT_HAS_JOB = "The applicant already has a job, so an interview cannot " + + "be scheduled."; + + private final Index applicantIndex; + private final LocalDateTime date; + private final Index positionIndex; + + /** + * Creates an AddApplicantCommand to add the specified {@code Interview} + */ + public AddInterviewCommand(Index applicantIndex, LocalDateTime date, Index positionIndex) { + requireNonNull(applicantIndex); + requireNonNull(date); + requireNonNull(positionIndex); + this.applicantIndex = applicantIndex; + this.date = date; + this.positionIndex = positionIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownApplicantList = model.getFilteredApplicantList(); + + if (applicantIndex.getZeroBased() >= lastShownApplicantList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_APPLICANT_DISPLAYED_INDEX); + } + Applicant applicantInInterview = lastShownApplicantList.get(applicantIndex.getZeroBased()); + if (applicantInInterview.isHired()) { + throw new CommandException(MESSAGE_APPLICANT_HAS_JOB); + } + List lastShownPositionList = model.getFilteredPositionList(); + if (positionIndex.getZeroBased() >= lastShownPositionList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_POSITION_DISPLAYED_INDEX); + } + Position positionInInterview = lastShownPositionList.get(positionIndex.getZeroBased()); + if (model.isSameApplicantPosition(applicantInInterview, positionInInterview)) { + throw new CommandException(String.format(Messages.MESSAGE_APPLICANT_SAME_POSITION, + applicantInInterview.getName().fullName, positionInInterview.getPositionName().positionName)); + } + if (!positionInInterview.canScheduleInterview()) { + throw new CommandException(MESSAGE_POSITION_NO_OPENING); + } + Interview interviewToAdd = new Interview(applicantInInterview, date, positionInInterview); + if (model.hasConflictingInterview(interviewToAdd)) { + throw new CommandException(Messages.MESSAGE_CONFLICTING_INTERVIEW); + } + model.addInterview(interviewToAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, interviewToAdd), getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.INTERVIEW; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddInterviewCommand // instanceof handles nulls + && applicantIndex.equals(((AddInterviewCommand) other).applicantIndex) + && date.equals(((AddInterviewCommand) other).date) + && positionIndex.equals(((AddInterviewCommand) other).positionIndex)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/interview/DeleteInterviewCommand.java b/src/main/java/seedu/address/logic/commands/interview/DeleteInterviewCommand.java new file mode 100644 index 00000000000..20c19cf8642 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/interview/DeleteInterviewCommand.java @@ -0,0 +1,63 @@ +package seedu.address.logic.commands.interview; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; + +public class DeleteInterviewCommand extends DeleteCommand { + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " -i : Deletes the interview identified by the index number used in the displayed interview list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " -i 1"; + + public static final String MESSAGE_DELETE_INTERVIEW_SUCCESS = "Deleted Interview: %1$s"; + + private final Index targetIndex; + + public DeleteInterviewCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredInterviewList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_INTERVIEW_DISPLAYED_INDEX); + } + Interview interviewToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteInterview(interviewToDelete); + + if (interviewToDelete.isPassedStatus()) { + Position oldPosition = interviewToDelete.getPosition(); + Position newPosition = interviewToDelete.getPosition().rejectOffer(); + model.updatePosition(oldPosition, newPosition); + } + return new CommandResult(String.format(MESSAGE_DELETE_INTERVIEW_SUCCESS, interviewToDelete), + getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.INTERVIEW; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteInterviewCommand // instanceof handles nulls + && targetIndex.equals(((DeleteInterviewCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/interview/EditInterviewCommand.java b/src/main/java/seedu/address/logic/commands/interview/EditInterviewCommand.java new file mode 100644 index 00000000000..927f5427a86 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/interview/EditInterviewCommand.java @@ -0,0 +1,244 @@ +package seedu.address.logic.commands.interview; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_INTERVIEW_DISPLAYED_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPLICANT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; + +/** + * Edits the details of an existing interview in the application. + */ +public class EditInterviewCommand extends EditCommand { + public static final String MESSAGE_USAGE = COMMAND_WORD + " -i: Edits the details of an interview in Hirelah\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_APPLICANT + "APPLICANT_INDEX" + + PREFIX_DATE + "DATE " + + PREFIX_POSITION + "POSITION_INDEX" + + "\nExample: " + COMMAND_WORD + " -i " + "1 " + + PREFIX_APPLICANT + "1" + + PREFIX_DATE + "2022-05-03 16:00 " + + PREFIX_POSITION + "2"; + + public static final String MESSAGE_NOT_EDITED = "At least one field in Interview to edit must be provided."; + public static final String MESSAGE_EDIT_INTERVIEW_SUCCESS = "Edited interview: %1$s"; + public static final String MESSAGE_NOT_PENDING = "Only interviews that are pending can be edited."; + public static final String MESSAGE_POSITION_NO_OPENING = "The position has no openings, so an interview cannot be" + + " scheduled."; + public static final String MESSAGE_APPLICANT_HAS_JOB = "The applicant already has a job, so an interview cannot " + + "be scheduled."; + + + private final Index index; + private final EditInterviewDescriptor editInterviewDescriptor; + + /** + * @param index of the interview in the filtered interview list to edit + * @param editInterviewDescriptor details to edit the interview with + */ + public EditInterviewCommand(Index index, EditInterviewDescriptor editInterviewDescriptor) { + requireNonNull(index); + requireNonNull(editInterviewDescriptor); + + this.index = index; + this.editInterviewDescriptor = new EditInterviewDescriptor(editInterviewDescriptor); + } + + /** + * Creates and returns a {@code Interview} with the details of {@code interviewToEdit} + * edited with {@code editInterviewDescriptor}. + */ + private static Interview createEditedInterview(Interview interviewToEdit, EditInterviewDescriptor + editInterviewDescriptor, Model model) throws CommandException { + assert interviewToEdit != null; + + Index updatedApplicantIndex = editInterviewDescriptor.getApplicantIndex().orElse(null); + Applicant updatedApplicant; + if (updatedApplicantIndex == null) { + updatedApplicant = interviewToEdit.getApplicant(); + } else { + List lastShownApplicantList = model.getFilteredApplicantList(); + if (updatedApplicantIndex.getZeroBased() >= lastShownApplicantList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_APPLICANT_DISPLAYED_INDEX); + } + updatedApplicant = lastShownApplicantList.get(updatedApplicantIndex.getZeroBased()); + } + + LocalDateTime updatedDate = editInterviewDescriptor.getDate().orElse(interviewToEdit.getDate()); + + Index updatedPositionIndex = editInterviewDescriptor.getPositionIndex().orElse(null); + Position updatedPosition; + if (updatedPositionIndex == null) { + updatedPosition = interviewToEdit.getPosition(); + } else { + List lastShownPositionList = model.getFilteredPositionList(); + if (updatedPositionIndex.getZeroBased() >= lastShownPositionList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_POSITION_DISPLAYED_INDEX); + } + updatedPosition = lastShownPositionList.get(updatedPositionIndex.getZeroBased()); + } + + Interview updatedInterview = new Interview(updatedApplicant, updatedDate, updatedPosition); + + return updatedInterview; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredInterviewList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(MESSAGE_INVALID_INTERVIEW_DISPLAYED_INDEX); + } + + Interview interviewToEdit = lastShownList.get(index.getZeroBased()); + + if (!interviewToEdit.getStatus().isPendingStatus()) { + throw new CommandException(MESSAGE_NOT_PENDING); + } + + Interview editedInterview = createEditedInterview(interviewToEdit, editInterviewDescriptor, model); + + boolean applicantEdited = !(interviewToEdit.getApplicant().equals(editedInterview.getApplicant())); + boolean positionEdited = !(interviewToEdit.getPosition().equals(editedInterview.getPosition())); + + if ((applicantEdited || positionEdited) + && model.isSameApplicantPosition(editedInterview.getApplicant(), editedInterview.getPosition())) { + throw new CommandException(String.format(Messages.MESSAGE_APPLICANT_SAME_POSITION, + editedInterview.getApplicant().getName().fullName, + editedInterview.getPosition().getPositionName().positionName)); + } else if (applicantEdited && editedInterview.getApplicant().isHired()) { + throw new CommandException(MESSAGE_APPLICANT_HAS_JOB); + } else if (positionEdited && !editedInterview.getPosition().canScheduleInterview()) { + throw new CommandException(MESSAGE_POSITION_NO_OPENING); + } + + boolean dateEdited = !(interviewToEdit.getDate().equals(editedInterview.getDate())); + if (dateEdited && model.hasConflictingInterview(editedInterview)) { + throw new CommandException(Messages.MESSAGE_CONFLICTING_INTERVIEW); + } + + if (!interviewToEdit.equals(editedInterview) && model.hasInterview(editedInterview)) { + throw new CommandException(Messages.MESSAGE_DUPLICATE_INTERVIEW); + } + + model.setInterview(interviewToEdit, editedInterview); + model.updateFilteredInterviewList(Model.PREDICATE_SHOW_ALL_INTERVIEWS); + + return new CommandResult(String.format(MESSAGE_EDIT_INTERVIEW_SUCCESS, editedInterview), getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.INTERVIEW; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles null + if (!(other instanceof seedu.address.logic.commands.position.EditPositionCommand)) { + return false; + } + + // state check + EditInterviewCommand e = (EditInterviewCommand) other; + return index.equals(e.index) && editInterviewDescriptor.equals(e.editInterviewDescriptor); + } + + /** + * Store the details to edit the interview with. Each non-empty field value will replace the + * corresponding field value of the interview. + */ + public static class EditInterviewDescriptor { + private Index applicantIndex; + private LocalDateTime date; + private Index positionIndex; + + public EditInterviewDescriptor() { + } + + /** + * Constructs an EditInterviewDescriptor. + * + * @param toCopy descriptor object to copy + */ + public EditInterviewDescriptor(EditInterviewDescriptor toCopy) { + setApplicantIndex(toCopy.applicantIndex); + setDate(toCopy.date); + setPositionIndex(toCopy.positionIndex); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(applicantIndex, date, positionIndex); + } + + public Optional getApplicantIndex() { + return Optional.ofNullable(applicantIndex); + } + + public void setApplicantIndex(Index applicantIndex) { + this.applicantIndex = applicantIndex; + } + + public Optional getDate() { + return Optional.ofNullable(date); + } + + public void setDate(LocalDateTime date) { + this.date = date; + } + + public Optional getPositionIndex() { + return Optional.ofNullable(positionIndex); + } + + public void setPositionIndex(Index positionIndex) { + this.positionIndex = positionIndex; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditInterviewDescriptor)) { + return false; + } + + // state check + EditInterviewDescriptor e = (EditInterviewDescriptor) other; + + return getApplicantIndex().equals(e.getApplicantIndex()) + && getDate().equals(e.getDate()) + && getPositionIndex().equals(e.getPositionIndex()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/interview/ExportInterviewCsvCommand.java b/src/main/java/seedu/address/logic/commands/interview/ExportInterviewCsvCommand.java new file mode 100644 index 00000000000..260bccee9e7 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/interview/ExportInterviewCsvCommand.java @@ -0,0 +1,26 @@ +package seedu.address.logic.commands.interview; + +import java.io.FileNotFoundException; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.exceptions.ExportCsvOpenException; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.ExportCsvCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +public class ExportInterviewCsvCommand extends ExportCsvCommand { + + public static final String MESSAGE_SUCCESS = "Interview CSV is successfully exported at export_csv/interview.csv"; + + @Override + public CommandResult execute(Model model) throws CommandException, FileNotFoundException, ExportCsvOpenException { + model.exportCsvInterview(); + return new CommandResult(MESSAGE_SUCCESS, getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.INTERVIEW; + } +} diff --git a/src/main/java/seedu/address/logic/commands/interview/FailInterviewCommand.java b/src/main/java/seedu/address/logic/commands/interview/FailInterviewCommand.java new file mode 100644 index 00000000000..80f98d2c71e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/interview/FailInterviewCommand.java @@ -0,0 +1,67 @@ +package seedu.address.logic.commands.interview; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.interview.Interview; + +public class FailInterviewCommand extends Command { + public static final String COMMAND_WORD = "fail"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_FAIL_INTERVIEW_SUCCESS = "Failed Interview: %1$s"; + public static final String MESSAGE_INTERVIEW_CANNOT_BE_FAILED = "The interview cannot be failed, " + + "because only pending interviews can be failed"; + + private final Index targetIndex; + + public FailInterviewCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredInterviewList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_INTERVIEW_DISPLAYED_INDEX); + } + + Interview interviewToFail = lastShownList.get(targetIndex.getZeroBased()); + if (!interviewToFail.isFailableInterview()) { + throw new CommandException(MESSAGE_INTERVIEW_CANNOT_BE_FAILED); + } + + Interview failedInterview = new Interview(interviewToFail.getApplicant(), interviewToFail.getDate(), + interviewToFail.getPosition()); + failedInterview.markAsFailed(); + model.setInterview(interviewToFail, failedInterview); + + return new CommandResult(String.format(MESSAGE_FAIL_INTERVIEW_SUCCESS, failedInterview), + getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.INTERVIEW; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FailInterviewCommand // instanceof handles nulls + && targetIndex.equals(((FailInterviewCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/interview/ListInterviewCommand.java b/src/main/java/seedu/address/logic/commands/interview/ListInterviewCommand.java new file mode 100644 index 00000000000..38caefc1c74 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/interview/ListInterviewCommand.java @@ -0,0 +1,124 @@ +package seedu.address.logic.commands.interview; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_ARGUMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_TYPE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SORT_ARGUMENT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_INTERVIEWS; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Comparator; +import java.util.function.Predicate; + +import seedu.address.commons.core.DataType; +import seedu.address.logic.FilterArgument; +import seedu.address.logic.FilterType; +import seedu.address.logic.SortArgument; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.ListCommand; +import seedu.address.model.Model; +import seedu.address.model.interview.Interview; +import seedu.address.model.interview.InterviewApplicantPredicate; +import seedu.address.model.interview.InterviewDateComparator; +import seedu.address.model.interview.InterviewDatePredicate; +import seedu.address.model.interview.InterviewPositionPredicate; +import seedu.address.model.interview.InterviewStatusPredicate; + +/** + * Lists interviews in HireLah to the user. + */ +public class ListInterviewCommand extends ListCommand { + + public static final String MESSAGE_USAGE = COMMAND_WORD + " -i: List interviews with optional parameters." + + "\nOptional parameters: " + + PREFIX_FILTER_TYPE + "FILTER_TYPE " + + PREFIX_FILTER_ARGUMENT + "FILTER_TYPE " + + PREFIX_SORT_ARGUMENT + "[asc/dsc] " + + "\nExample: " + COMMAND_WORD + " -i " + + PREFIX_FILTER_TYPE + "appl " + + PREFIX_FILTER_ARGUMENT + "John Doe " + + PREFIX_SORT_ARGUMENT + "asc "; + + public static final String MESSAGE_SUCCESS = "Listed %1$d interviews"; + + private FilterType filterType; + private FilterArgument filterArgument; + private SortArgument sortArgument; + + /** + * Creates an ListInterviewCommand to display all {@code Interview} + */ + public ListInterviewCommand() { + filterType = null; + filterArgument = null; + sortArgument = null; + } + + /** + * Creates an ListInterviewCommand to filter and display {@code Interview} + */ + public ListInterviewCommand(FilterType filterType, FilterArgument filterArgument) { + this.filterType = filterType; + this.filterArgument = filterArgument; + } + + public ListInterviewCommand(SortArgument sortArgument) { + this.sortArgument = sortArgument; + } + + /** + * Creates an ListApplicantCommand to filter and sort then display {@code Interview} + */ + public ListInterviewCommand(FilterType filterType, FilterArgument filterArgument, SortArgument sortArgument) { + this.filterType = filterType; + this.filterArgument = filterArgument; + this.sortArgument = sortArgument; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + if (filterType != null && filterArgument != null && sortArgument != null) { + Predicate predicate = getFilterPredicate(filterType, filterArgument); + Comparator comparator = new InterviewDateComparator(sortArgument.toString()); + model.updateFilterAndSortInterviewList(predicate, comparator); + } else if (filterType != null && filterArgument != null) { + Predicate predicate = getFilterPredicate(filterType, filterArgument); + model.updateFilteredInterviewList(predicate); + } else if (sortArgument != null) { + Comparator comparator = new InterviewDateComparator(sortArgument.toString()); + model.updateSortInterviewList(comparator); + } else { + model.updateFilteredInterviewList(PREDICATE_SHOW_ALL_INTERVIEWS); + } + + return new CommandResult( + String.format(MESSAGE_SUCCESS, model.getFilteredInterviewList().size()), getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.INTERVIEW; + } + + /** + * Returns the suitable {@code Predicate} based on the given {@code filterType} and {@code filterArgument} + */ + public Predicate getFilterPredicate(FilterType filterType, FilterArgument filterArgument) { + if (filterType.type.equals("appl")) { + String[] applicantNameKeywords = filterArgument.toString().split("\\s+"); + return new InterviewApplicantPredicate(Arrays.asList(applicantNameKeywords)); + } else if (filterType.type.equals("pos")) { + String[] positionNameKeywords = filterArgument.toString().split("\\s+"); + return new InterviewPositionPredicate(Arrays.asList(positionNameKeywords)); + } else if (filterType.type.equals("date")) { + return new InterviewDatePredicate(LocalDate.parse(filterArgument.toString())); + } else if (filterType.type.equals("status")) { + return new InterviewStatusPredicate(filterArgument.toString()); + } + + assert true : "Filter type should be valid"; + return null; + } +} diff --git a/src/main/java/seedu/address/logic/commands/interview/PassInterviewCommand.java b/src/main/java/seedu/address/logic/commands/interview/PassInterviewCommand.java new file mode 100644 index 00000000000..4d8d718a1c5 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/interview/PassInterviewCommand.java @@ -0,0 +1,76 @@ +package seedu.address.logic.commands.interview; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; + +public class PassInterviewCommand extends Command { + public static final String COMMAND_WORD = "pass"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_PASS_INTERVIEW_SUCCESS = "Passed Interview: %1$s"; + public static final String MESSAGE_INTERVIEW_NOT_PENDING_STATUS = "Only pending interviews can be passed"; + public static final String MESSAGE_INTERVIEW_CANNOT_BE_PASSED = "The interview cannot be passed, " + + "as the number of current offers will exceed the number of available positions"; + public static final String MESSAGE_APPLICANT_HAS_JOB = "The applicant already has a job, so this interview " + + "cannot be passed"; + private final Index targetIndex; + + public PassInterviewCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredInterviewList(); + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_INTERVIEW_DISPLAYED_INDEX); + } + Interview interviewToPass = lastShownList.get(targetIndex.getZeroBased()); + if (!interviewToPass.isPendingStatus()) { + throw new CommandException(MESSAGE_INTERVIEW_NOT_PENDING_STATUS); + } + if (!interviewToPass.isPassableInterview()) { + throw new CommandException(MESSAGE_INTERVIEW_CANNOT_BE_PASSED); + } + if (interviewToPass.getApplicant().isHired()) { + throw new CommandException(MESSAGE_APPLICANT_HAS_JOB); + } + Position oldPosition = interviewToPass.getPosition(); + Position newPosition = interviewToPass.getPosition().extendOffer(); + Interview passedInterview = new Interview(interviewToPass.getApplicant(), interviewToPass.getDate(), + newPosition); + passedInterview.markAsPassed(); + model.setInterview(interviewToPass, passedInterview); + model.updatePosition(oldPosition, newPosition); + + return new CommandResult(String.format(MESSAGE_PASS_INTERVIEW_SUCCESS, passedInterview), + getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.INTERVIEW; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PassInterviewCommand // instanceof handles nulls + && targetIndex.equals(((PassInterviewCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/interview/RejectInterviewCommand.java b/src/main/java/seedu/address/logic/commands/interview/RejectInterviewCommand.java new file mode 100644 index 00000000000..28737cd0b70 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/interview/RejectInterviewCommand.java @@ -0,0 +1,74 @@ +package seedu.address.logic.commands.interview; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; + +public class RejectInterviewCommand extends Command { + public static final String COMMAND_WORD = "reject"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_REJECT_INTERVIEW_SUCCESS = "Rejected Interview: %1$s"; + public static final String MESSAGE_INTERVIEW_CANNOT_BE_REJECTED = "Only passed interviews can be rejected!"; + + private final Index targetIndex; + + public RejectInterviewCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredInterviewList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_INTERVIEW_DISPLAYED_INDEX); + } + + Interview interviewToReject = lastShownList.get(targetIndex.getZeroBased()); + + if (!interviewToReject.isRejectableInterview()) { + throw new CommandException(MESSAGE_INTERVIEW_CANNOT_BE_REJECTED); + } + + Position oldPosition = interviewToReject.getPosition(); + Position newPosition = interviewToReject.getPosition().rejectOffer(); + Interview rejectedInterview = new Interview(interviewToReject.getApplicant(), interviewToReject.getDate(), + newPosition); + + rejectedInterview.markAsPassed(); + rejectedInterview.markAsRejected(); + model.setInterview(interviewToReject, rejectedInterview); + model.updatePosition(oldPosition, newPosition); + + return new CommandResult(String.format(MESSAGE_REJECT_INTERVIEW_SUCCESS, rejectedInterview), + getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.INTERVIEW; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof RejectInterviewCommand // instanceof handles nulls + && targetIndex.equals(((RejectInterviewCommand) other).targetIndex)); // state check + } +} + diff --git a/src/main/java/seedu/address/logic/commands/position/AddPositionCommand.java b/src/main/java/seedu/address/logic/commands/position/AddPositionCommand.java new file mode 100644 index 00000000000..8f165c51e9c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/position/AddPositionCommand.java @@ -0,0 +1,70 @@ +package seedu.address.logic.commands.position; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NUM_OPENINGS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REQUIREMENT; + +import seedu.address.commons.core.DataType; +import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.position.Position; + +/** + * Adds a position in the application. + */ +public class AddPositionCommand extends AddCommand { + + public static final String MESSAGE_USAGE = COMMAND_WORD + " -p: Adds a position to the application. " + + "Parameters: " + + PREFIX_POSITION + "POSITION_NAME" + + PREFIX_NUM_OPENINGS + "NUM_OPENINGS " + + PREFIX_DESCRIPTION + "DESCRIPTION " + + "[" + PREFIX_REQUIREMENT + "REQUIREMENT]...\n" + + "Example: " + COMMAND_WORD + " -p " + + PREFIX_POSITION + "Junior Back-end Software Engineer " + + PREFIX_NUM_OPENINGS + "3 " + + PREFIX_DESCRIPTION + "Must be able to work remotely. Teams are assigned during on-boarding process. " + + PREFIX_REQUIREMENT + "Computer Science Bachelors " + + PREFIX_REQUIREMENT + "Experience with SQL"; + + public static final String MESSAGE_SUCCESS = "New position added: %1$s"; + public static final String MESSAGE_DUPLICATE_POSITION = "This position already exists in HireLah application"; + + private final Position toAdd; + + /** + * Creates an AddPositionCommand to add the specified {@code position} + */ + public AddPositionCommand(Position position) { + requireNonNull(position); + toAdd = position; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasPosition(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_POSITION); + } + + model.addPosition(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd), getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.POSITION; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddPositionCommand // instanceof handles nulls + && toAdd.equals(((AddPositionCommand) other).toAdd)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/position/DeletePositionCommand.java b/src/main/java/seedu/address/logic/commands/position/DeletePositionCommand.java new file mode 100644 index 00000000000..ce52ed74db3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/position/DeletePositionCommand.java @@ -0,0 +1,79 @@ +package seedu.address.logic.commands.position; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; + +/** + * Deletes a position identified using it's displayed index from the application, + * and the interviews associated with the position as well. + */ +public class DeletePositionCommand extends DeleteCommand { + + public static final String MESSAGE_USAGE = COMMAND_WORD + + " -p : Deletes the position identified by the index number used in the displayed position list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " -p 1"; + + public static final String MESSAGE_DELETE_POSITION_SUCCESS = "Deleted Position: %1$s"; + + public static final String MESSAGE_DELETE_INTERVIEWS = "Deleted %d related interview(s)"; + + private final Index targetIndex; + + private final Logger logger = LogsCenter.getLogger(DeletePositionCommand.class); + + public DeletePositionCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPositionList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_POSITION_DISPLAYED_INDEX); + } + + Position positionToDelete = lastShownList.get(targetIndex.getZeroBased()); + + ArrayList interviewsToDelete = model.getPositionsInterviews(positionToDelete); + for (Interview i : interviewsToDelete) { + model.deleteInterview(i); + logger.log(Level.INFO, String.format("Deleted interview: %1$s", i)); + } + + + model.deletePosition(positionToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_POSITION_SUCCESS, positionToDelete) + "\n" + + String.format(MESSAGE_DELETE_INTERVIEWS, interviewsToDelete.size()), + getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.POSITION; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeletePositionCommand // instanceof handles nulls + && targetIndex.equals(((DeletePositionCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/position/EditPositionCommand.java b/src/main/java/seedu/address/logic/commands/position/EditPositionCommand.java new file mode 100644 index 00000000000..c8cb4fb22e2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/position/EditPositionCommand.java @@ -0,0 +1,240 @@ +package seedu.address.logic.commands.position; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.DataTypeFlags.FLAG_POSITION; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_POSITION_DISPLAYED_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NUM_OPENINGS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REQUIREMENT; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.position.Description; +import seedu.address.model.position.Position; +import seedu.address.model.position.PositionName; +import seedu.address.model.position.PositionOpenings; +import seedu.address.model.position.Requirement; + +/** + * Edits the details of an existing position in the position. + */ +public class EditPositionCommand extends EditCommand { + + public static final String MESSAGE_USAGE = COMMAND_WORD + " -" + FLAG_POSITION + + ": Edits the details of the position identified " + + "by the index number used in the displayed position list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_POSITION + "POSITION_NAME] " + + "[" + PREFIX_DESCRIPTION + "DESCRIPTION] " + + "[" + PREFIX_NUM_OPENINGS + "NUM_OPENINGS] " + + "[" + PREFIX_REQUIREMENT + "REQUIREMENT]...\n" + + "Example: " + COMMAND_WORD + " -" + FLAG_POSITION + " 1 " + + PREFIX_NUM_OPENINGS + "2 " + + PREFIX_REQUIREMENT + "First class honours "; + + public static final String MESSAGE_NOT_EDITED = "At least one field in Position to edit must be provided."; + public static final String MESSAGE_NOT_VALID_OPENINGS = "New number of openings cannot be less than the current " + + "number of outstanding offers. Please reject some of the existing candidates before changing the " + + "number of openings."; + public static final String MESSAGE_DUPLICATE_POSITION = "This position already exists in HireLah."; + public static final String MESSAGE_EDIT_POSITION_SUCCESS = "Edited Position: %1$s"; + + private static final Logger logger = LogsCenter.getLogger(EditPositionCommand.class); + + private final Index index; + private final EditPositionDescriptor editPositionDescriptor; + + /** + * @param index of the position in the filtered position list to edit + * @param editPositionDescriptor details to edit the position with + */ + public EditPositionCommand(Index index, EditPositionDescriptor editPositionDescriptor) { + requireNonNull(index); + requireNonNull(editPositionDescriptor); + + this.index = index; + this.editPositionDescriptor = new EditPositionDescriptor(editPositionDescriptor); + } + + /** + * Creates and returns a {@code Position} with the details of {@code positionToEdit} + * edited with {@code editPositionDescriptor}. + */ + private static Position createEditedPosition(Position positionToEdit, EditPositionDescriptor + editPositionDescriptor) throws CommandException { + assert positionToEdit != null; + + PositionName updatedPositionName = editPositionDescriptor.getPositionName() + .orElse(positionToEdit.getPositionName()); + Description updatedDescription = editPositionDescriptor.getDescription() + .orElse(positionToEdit.getDescription()); + PositionOpenings updatedOpenings = editPositionDescriptor.getPositionOpenings() + .orElse(positionToEdit.getPositionOpenings()); + Set updatedRequirements = editPositionDescriptor.getRequirements() + .orElse(positionToEdit.getRequirements()); + + Position updatedPosition = new Position(updatedPositionName, updatedDescription, + updatedOpenings, positionToEdit.getPositionOffers(), updatedRequirements); + if (!updatedPosition.isValidOpeningsToOffers()) { + logger.log(Level.WARNING, "Editing position to have less openings than offers is not allowed"); + throw new CommandException(MESSAGE_NOT_VALID_OPENINGS); + } + return updatedPosition; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPositionList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(MESSAGE_INVALID_POSITION_DISPLAYED_INDEX); + } + + Position positionToEdit = lastShownList.get(index.getZeroBased()); + Position editedPosition = createEditedPosition(positionToEdit, editPositionDescriptor); + + if (!positionToEdit.isSamePosition(editedPosition) && model.hasPosition(editedPosition)) { + throw new CommandException(MESSAGE_DUPLICATE_POSITION); + } + + model.updatePosition(positionToEdit, editedPosition); + model.updateFilteredPositionList(Model.PREDICATE_SHOW_ALL_POSITIONS); + + return new CommandResult(String.format(MESSAGE_EDIT_POSITION_SUCCESS, editedPosition), getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.POSITION; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles null + if (!(other instanceof EditPositionCommand)) { + return false; + } + + // state check + EditPositionCommand e = (EditPositionCommand) other; + return index.equals(e.index) && editPositionDescriptor.equals(e.editPositionDescriptor); + } + + /** + * Store the details to edit the position with. Each non-empty field value will replace the + * corresponding field value of the position. + */ + public static class EditPositionDescriptor { + private PositionName positionName; + private Description description; + private PositionOpenings positionOpenings; + private Set requirements; + + public EditPositionDescriptor() { + } + + /** + * Constructs an EditPositionDescriptor. + * + * @param toCopy descriptor object to copy + */ + public EditPositionDescriptor(EditPositionDescriptor toCopy) { + setPositionName(toCopy.positionName); + setDescription(toCopy.description); + setPositionOpenings(toCopy.positionOpenings); + setRequirements(toCopy.requirements); + + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(positionName, description, positionOpenings, requirements); + } + + public Optional getPositionName() { + return Optional.ofNullable(positionName); + } + + public void setPositionName(PositionName positionName) { + this.positionName = positionName; + } + + public Optional getDescription() { + return Optional.ofNullable(description); + } + + public void setDescription(Description description) { + this.description = description; + } + + public Optional getPositionOpenings() { + return Optional.ofNullable(positionOpenings); + } + + public void setPositionOpenings(PositionOpenings positionOpenings) { + this.positionOpenings = positionOpenings; + } + + /** + * Returns an unmodifiable requirement set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code requirements} is null. + */ + public Optional> getRequirements() { + return (requirements != null) ? Optional.of(Collections.unmodifiableSet(requirements)) : Optional.empty(); + } + + /** + * Sets {@code requirements} to this object's {@code requirements}. + * A defensive copy of {@code requirements} is used internally. + */ + public void setRequirements(Set requirements) { + this.requirements = (requirements != null) ? new HashSet<>(requirements) : null; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditPositionDescriptor)) { + return false; + } + + // state check + EditPositionDescriptor e = (EditPositionDescriptor) other; + + return getPositionName().equals(e.getPositionName()) + && getDescription().equals(e.getDescription()) + && getPositionOpenings().equals(e.getPositionOpenings()) + && getRequirements().equals(e.getRequirements()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/position/ExportPositionCsvCommand.java b/src/main/java/seedu/address/logic/commands/position/ExportPositionCsvCommand.java new file mode 100644 index 00000000000..aeb02626f89 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/position/ExportPositionCsvCommand.java @@ -0,0 +1,26 @@ +package seedu.address.logic.commands.position; + +import java.io.FileNotFoundException; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.exceptions.ExportCsvOpenException; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.ExportCsvCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +public class ExportPositionCsvCommand extends ExportCsvCommand { + + public static final String MESSAGE_SUCCESS = "Position CSV is successfully exported at export_csv/position.csv"; + + @Override + public CommandResult execute(Model model) throws CommandException, FileNotFoundException, ExportCsvOpenException { + model.exportCsvPosition(); + return new CommandResult(MESSAGE_SUCCESS, getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.POSITION; + } +} diff --git a/src/main/java/seedu/address/logic/commands/position/ListPositionCommand.java b/src/main/java/seedu/address/logic/commands/position/ListPositionCommand.java new file mode 100644 index 00000000000..51ab315b0d8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/position/ListPositionCommand.java @@ -0,0 +1,118 @@ +package seedu.address.logic.commands.position; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_ARGUMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_TYPE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SORT_ARGUMENT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_POSITIONS; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.function.Predicate; + +import seedu.address.commons.core.DataType; +import seedu.address.logic.FilterArgument; +import seedu.address.logic.FilterType; +import seedu.address.logic.SortArgument; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.ListCommand; +import seedu.address.model.Model; +import seedu.address.model.position.Position; +import seedu.address.model.position.PositionNameComparator; +import seedu.address.model.position.PositionNamePredicate; +import seedu.address.model.position.PositionRequirementPredicate; + +/** + * Lists positions in HireLah to the user. + */ +public class ListPositionCommand extends ListCommand { + + public static final String MESSAGE_USAGE = COMMAND_WORD + " -p: List positions with optional parameters." + + "\nOptional parameters: " + + PREFIX_FILTER_TYPE + "FILTER_TYPE " + + PREFIX_FILTER_ARGUMENT + "FILTER_TYPE " + + PREFIX_SORT_ARGUMENT + "[asc/dsc] " + + "\nExample: " + COMMAND_WORD + " -p " + + PREFIX_FILTER_TYPE + "req " + + PREFIX_FILTER_ARGUMENT + "Java " + + PREFIX_SORT_ARGUMENT + "asc "; + + public static final String MESSAGE_SUCCESS = "Listed %1$d positions"; + + private FilterType filterType; + private FilterArgument filterArgument; + private SortArgument sortArgument; + + /** + * Creates an ListPositionCommand to display all {@code Position} + */ + public ListPositionCommand() { + filterType = null; + filterArgument = null; + sortArgument = null; + } + + /** + * Creates an ListPositionCommand to filter and display {@code Position} + */ + public ListPositionCommand(FilterType filterType, FilterArgument filterArgument) { + this.filterType = filterType; + this.filterArgument = filterArgument; + } + + public ListPositionCommand(SortArgument sortArgument) { + this.sortArgument = sortArgument; + } + + /** + * Creates an ListApplicantCommand to filter and sort then display {@code Position} + */ + public ListPositionCommand(FilterType filterType, FilterArgument filterArgument, SortArgument sortArgument) { + this.filterType = filterType; + this.filterArgument = filterArgument; + this.sortArgument = sortArgument; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + + if (filterType != null && filterArgument != null && sortArgument != null) { + Predicate predicate = getFilterPredicate(filterType, filterArgument); + Comparator comparator = new PositionNameComparator(sortArgument.toString()); + model.updateFilterAndSortPositionList(predicate, comparator); + } else if (filterType != null && filterArgument != null) { + Predicate predicate = getFilterPredicate(filterType, filterArgument); + model.updateFilteredPositionList(predicate); + } else if (sortArgument != null) { + Comparator comparator = new PositionNameComparator(sortArgument.toString()); + model.updateSortPositionList(comparator); + } else { + model.updateFilteredPositionList(PREDICATE_SHOW_ALL_POSITIONS); + } + + return new CommandResult( + String.format(MESSAGE_SUCCESS, model.getFilteredPositionList().size()), getCommandDataType()); + } + + @Override + public DataType getCommandDataType() { + return DataType.POSITION; + } + + /** + * Returns the suitable {@code Predicate} based on the given {@code filterType} and {@code filterArgument} + */ + public Predicate getFilterPredicate(FilterType filterType, FilterArgument filterArgument) { + if (filterType.type.equals("name")) { + String[] nameKeywords = filterArgument.toString().split("\\s+"); + return new PositionNamePredicate(Arrays.asList(nameKeywords)); + } else if ((filterType.type.equals("req"))) { + String[] reqKeywords = filterArgument.toString().split("\\s+"); + return new PositionRequirementPredicate(Arrays.asList(reqKeywords)); + } + + assert true : "Filter type should be valid"; + return null; + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 3b8bfa035e8..fadc07fb0c8 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -1,60 +1,48 @@ package seedu.address.logic.parser; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.commons.core.DataTypeFlags.FLAG_APPLICANT; +import static seedu.address.commons.core.DataTypeFlags.FLAG_INTERVIEW; +import static seedu.address.commons.core.DataTypeFlags.FLAG_POSITION; -import java.util.Set; -import java.util.stream.Stream; +import java.util.logging.Level; +import java.util.logging.Logger; +import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.parser.applicants.AddApplicantCommandParser; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import seedu.address.logic.parser.interview.AddInterviewCommandParser; +import seedu.address.logic.parser.position.AddPositionCommandParser; + /** - * Parses input arguments and creates a new AddCommand object + * Parses add command and calls the respective AddXCommandParsers according to the flag. */ public class AddCommandParser implements Parser { + private final Logger logger = LogsCenter.getLogger(AddCommandParser.class); + /** - * Parses the given {@code String} of arguments in the context of the AddCommand - * and returns an AddCommand object for execution. + * Parses the given {@code String} of arguments in the context of the AddApplicantCommand, + * calls the respective AddXCommandParsers according to the flag specified + * and returns an AddApplicantCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ public AddCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + char flag = ArgumentTokenizer.getFlag(args.trim()); + String argsWithoutFlag = ArgumentTokenizer.removeFlag(args.trim()); + + if (flag == FLAG_APPLICANT) { + logger.log(Level.INFO, "Add command for applicant parsed"); + return new AddApplicantCommandParser().parse(argsWithoutFlag); + } else if (flag == FLAG_INTERVIEW) { + logger.log(Level.INFO, "Add command for interview parsed"); + return new AddInterviewCommandParser().parse(argsWithoutFlag); + } else if (flag == FLAG_POSITION) { + logger.log(Level.INFO, "Add command for position parsed"); + return new AddPositionCommandParser().parse(argsWithoutFlag); } - - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); + logger.log(Level.WARNING, "Add command did not find valid flag and did not throw an exception"); + return null; } - - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); - } - } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..60b60924dd3 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java @@ -1,10 +1,18 @@ package seedu.address.logic.parser; +import static seedu.address.commons.core.DataTypeFlags.FLAG_APPLICANT; +import static seedu.address.commons.core.DataTypeFlags.FLAG_INTERVIEW; +import static seedu.address.commons.core.DataTypeFlags.FLAG_POSITION; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_FLAG; +import static seedu.address.commons.core.Messages.MESSAGE_NO_FLAG; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +import seedu.address.logic.parser.exceptions.ParseException; + /** * Tokenizes arguments string of the form: {@code preamble value value ...}
* e.g. {@code some preamble text t/ 11.00 t/12.00 k/ m/ July} where prefixes are {@code t/ k/ m/}.
@@ -145,4 +153,35 @@ Prefix getPrefix() { } } + /** + * Gets the type flag from the given argument. + * + * @param argsString Arguments string of the form: {@code preamble value value ...} + * @return The flag of the command + * @throws ParseException When a flag is not found or when the flag is invalid; + */ + public static char getFlag(String argsString) throws ParseException { + if (argsString.equals("") || argsString.charAt(0) != '-') { + throw new ParseException(MESSAGE_NO_FLAG); + } + + char flag = argsString.charAt(1); + char whitespace = argsString.length() <= 2 ? ' ' : argsString.charAt(2); + + if (whitespace != ' ' || (flag != FLAG_APPLICANT && flag != FLAG_INTERVIEW && flag != FLAG_POSITION)) { + throw new ParseException(MESSAGE_INVALID_FLAG); + } + + return flag; + } + + /** + * Removes the type flag from the given argument. + * + * @param argsString Arguments string of the form: {@code preamble value value ...} + * @return The same argument without the type flag. + */ + public static String removeFlag(String argsString) { + return argsString.substring(2); + } } diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..48f75ece205 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -6,10 +6,25 @@ public class CliSyntax { /* Prefix definitions */ + // Applicant parser public static final Prefix PREFIX_NAME = new Prefix("n/"); public static final Prefix PREFIX_PHONE = new Prefix("p/"); public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); - + public static final Prefix PREFIX_AGE = new Prefix("ag/"); + public static final Prefix PREFIX_GENDER = new Prefix("g/"); + // Interview parser + public static final Prefix PREFIX_APPLICANT = new Prefix("a/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_POSITION = new Prefix("p/"); + // Position parser + public static final Prefix PREFIX_NUM_OPENINGS = new Prefix("o/"); + public static final Prefix PREFIX_DESCRIPTION = new Prefix("d/"); + public static final Prefix PREFIX_REQUIREMENT = new Prefix("r/"); + // List filter parser + public static final Prefix PREFIX_FILTER_TYPE = new Prefix("f/"); + public static final Prefix PREFIX_FILTER_ARGUMENT = new Prefix("a/"); + // List sort parser + public static final Prefix PREFIX_SORT_ARGUMENT = new Prefix("s/"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 522b93081cc..086d2f5395c 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -1,29 +1,39 @@ package seedu.address.logic.parser; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.DataTypeFlags.FLAG_APPLICANT; +import static seedu.address.commons.core.DataTypeFlags.FLAG_INTERVIEW; +import static seedu.address.commons.core.DataTypeFlags.FLAG_POSITION; -import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.parser.applicants.DeleteApplicantCommandParser; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.logic.parser.interview.DeleteInterviewCommandParser; +import seedu.address.logic.parser.position.DeletePositionCommandParser; /** - * Parses input arguments and creates a new DeleteCommand object + * Parses delete command and calls the respective DeleteXCommandParsers according to the flag. */ public class DeleteCommandParser implements Parser { /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. + * Parses the given {@code String} of arguments in the context of the DeleteApplicantCommand, + * calls the respective DeleteXCommandParsers according to the flag specified + * and returns an DeleteApplicantCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ public DeleteCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); - } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + char flag = ArgumentTokenizer.getFlag(args.trim()); + String argsWithoutFlag = ArgumentTokenizer.removeFlag(args.trim()); + + if (flag == FLAG_APPLICANT) { + return new DeleteApplicantCommandParser().parse(argsWithoutFlag); + } else if (flag == FLAG_INTERVIEW) { + return new DeleteInterviewCommandParser().parse(argsWithoutFlag); + } else if (flag == FLAG_POSITION) { + return new DeletePositionCommandParser().parse(argsWithoutFlag); } + + return null; } } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 845644b7dea..9ae8c633d6f 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -1,82 +1,36 @@ package seedu.address.logic.parser; -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.commons.core.DataTypeFlags.FLAG_APPLICANT; +import static seedu.address.commons.core.DataTypeFlags.FLAG_INTERVIEW; +import static seedu.address.commons.core.DataTypeFlags.FLAG_POSITION; -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.parser.applicants.EditApplicantCommandParser; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; +import seedu.address.logic.parser.interview.EditInterviewCommandParser; +import seedu.address.logic.parser.position.EditPositionCommandParser; /** - * Parses input arguments and creates a new EditCommand object + * Parses edit command and calls the respective EditXCommandParsers according to the flag. */ public class EditCommandParser implements Parser { - /** - * Parses the given {@code String} of arguments in the context of the EditCommand - * and returns an EditCommand object for execution. + * Parses the given {@code String} of arguments in the context of the AddApplicantCommand, + * calls the respective AddXCommandParsers according to the flag specified + * and returns an AddApplicantCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ public EditCommand parse(String args) throws ParseException { - requireNonNull(args); - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - Index index; - - try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); - } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); - } - - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); - if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); - } - if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); - } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + char flag = ArgumentTokenizer.getFlag(args.trim()); + String argsWithoutFlag = ArgumentTokenizer.removeFlag(args.trim()); + + if (flag == FLAG_APPLICANT) { + return new EditApplicantCommandParser().parse(argsWithoutFlag); + } else if (flag == FLAG_INTERVIEW) { + return new EditInterviewCommandParser().parse(argsWithoutFlag); + } else if (flag == FLAG_POSITION) { + return new EditPositionCommandParser().parse(argsWithoutFlag); } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); - - if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); - } - - return new EditCommand(index, editPersonDescriptor); + return null; } - - /** - * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. - * If {@code tags} contain only one element which is an empty string, it will be parsed into a - * {@code Set} containing zero tags. - */ - private Optional> parseTagsForEdit(Collection tags) throws ParseException { - assert tags != null; - - if (tags.isEmpty()) { - return Optional.empty(); - } - Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); - } - } diff --git a/src/main/java/seedu/address/logic/parser/ExportCsvCommandParser.java b/src/main/java/seedu/address/logic/parser/ExportCsvCommandParser.java new file mode 100644 index 00000000000..d69c00796dc --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ExportCsvCommandParser.java @@ -0,0 +1,39 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.DataTypeFlags.FLAG_APPLICANT; +import static seedu.address.commons.core.DataTypeFlags.FLAG_INTERVIEW; +import static seedu.address.commons.core.DataTypeFlags.FLAG_POSITION; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.ExportCsvCommand; +import seedu.address.logic.commands.applicant.ExportApplicantCsvCommand; +import seedu.address.logic.commands.interview.ExportInterviewCsvCommand; +import seedu.address.logic.commands.position.ExportPositionCsvCommand; +import seedu.address.logic.parser.exceptions.ParseException; + + +public class ExportCsvCommandParser implements Parser { + + private final Logger logger = LogsCenter.getLogger(ExportCsvCommandParser.class); + + @Override + public ExportCsvCommand parse(String args) throws ParseException { + char flag = ArgumentTokenizer.getFlag(args.trim()); + + if (flag == FLAG_APPLICANT) { + logger.log(Level.INFO, "Export csv for applicant"); + return new ExportApplicantCsvCommand(); + } else if (flag == FLAG_INTERVIEW) { + logger.log(Level.INFO, "Export csv for interview"); + return new ExportInterviewCsvCommand(); + } else if (flag == FLAG_POSITION) { + logger.log(Level.INFO, "Export csv for position"); + return new ExportPositionCsvCommand(); + } + logger.log(Level.WARNING, "Export command did not find valid flag and did not throw an exception"); + return null; + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java deleted file mode 100644 index 4fb71f23103..00000000000 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ /dev/null @@ -1,33 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import java.util.Arrays; - -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Parses input arguments and creates a new FindCommand object - */ -public class FindCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the FindCommand - * and returns a FindCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } - - String[] nameKeywords = trimmedArgs.split("\\s+"); - - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/GenericListParser.java b/src/main/java/seedu/address/logic/parser/GenericListParser.java new file mode 100644 index 00000000000..50c387ba550 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/GenericListParser.java @@ -0,0 +1,62 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_ARGUMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_TYPE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SORT_ARGUMENT; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.applicant.ListApplicantCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public abstract class GenericListParser implements Parser { + + @Override + public T parse(String userInput) throws ParseException { + if (userInput.equals("")) { + return returnFullList(); + } + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(userInput, PREFIX_FILTER_TYPE, PREFIX_FILTER_ARGUMENT, + PREFIX_SORT_ARGUMENT); + boolean isFilterTypeExist = argMultimap.getValue(PREFIX_FILTER_TYPE).isPresent(); + boolean isFilterArgumentExist = argMultimap.getValue(PREFIX_FILTER_ARGUMENT).isPresent(); + boolean isSortArgumentExist = argMultimap.getValue(PREFIX_SORT_ARGUMENT).isPresent(); + + if (isFilterArgumentExist && isFilterTypeExist && isSortArgumentExist) { + return parseFilterAndSort(argMultimap); + } else if (isFilterArgumentExist && isFilterTypeExist) { + return parseFilter(argMultimap); + } else if (isSortArgumentExist) { + return parseSort(argMultimap); + } else { + throw new ParseException(String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + ListApplicantCommand.MESSAGE_USAGE)); + } + } + + public abstract T parseFilterAndSort(ArgumentMultimap argMultimap) throws ParseException; + + /** + * Returns {T} command + * @return + */ + public abstract T returnFullList(); + + /** + * Parses the given {@code String} of arguments in the context of performing sort feature + * and returns an {T} object for execution. + * @param argMultimap The input arguments string + * @return {T} object with respective sort argument for execution + * @throws ParseException if the user input does not conform the expected sort format + */ + public abstract T parseSort(ArgumentMultimap argMultimap) throws ParseException; + + /** + * Parses the given {@code String} of arguments in the context of performing filter feature + * and returns an {T} object for execution. + * @param argMultimap The input arguments string + * @return {T} object with respective filter type and filter argument for execution + * @throws ParseException if the user input does not conform the expected filter format + */ + public abstract T parseFilter(ArgumentMultimap argMultimap) throws ParseException; +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/HireLahParser.java similarity index 60% rename from src/main/java/seedu/address/logic/parser/AddressBookParser.java rename to src/main/java/seedu/address/logic/parser/HireLahParser.java index 1e466792b46..c5461b42ac4 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/HireLahParser.java @@ -12,15 +12,24 @@ import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ExportCsvCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.help.HelpCommand; +import seedu.address.logic.commands.interview.AcceptInterviewCommand; +import seedu.address.logic.commands.interview.FailInterviewCommand; +import seedu.address.logic.commands.interview.PassInterviewCommand; +import seedu.address.logic.commands.interview.RejectInterviewCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.logic.parser.help.HelpCommandParser; +import seedu.address.logic.parser.interview.AcceptInterviewCommandParser; +import seedu.address.logic.parser.interview.FailInterviewCommandParser; +import seedu.address.logic.parser.interview.PassInterviewCommandParser; +import seedu.address.logic.parser.interview.RejectInterviewCommandParser; /** * Parses user input. */ -public class AddressBookParser { +public class HireLahParser { /** * Used for initial separation of command word and args. @@ -56,18 +65,28 @@ public Command parseCommand(String userInput) throws ParseException { case ClearCommand.COMMAND_WORD: return new ClearCommand(); - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - case ListCommand.COMMAND_WORD: - return new ListCommand(); + return new ListCommandParser().parse(arguments); case ExitCommand.COMMAND_WORD: return new ExitCommand(); case HelpCommand.COMMAND_WORD: - return new HelpCommand(); + return new HelpCommandParser().parse(arguments); + + case PassInterviewCommand.COMMAND_WORD: + return new PassInterviewCommandParser().parse(arguments); + + case FailInterviewCommand.COMMAND_WORD: + return new FailInterviewCommandParser().parse(arguments); + + case AcceptInterviewCommand.COMMAND_WORD: + return new AcceptInterviewCommandParser().parse(arguments); + case RejectInterviewCommand.COMMAND_WORD: + return new RejectInterviewCommandParser().parse(arguments); + case ExportCsvCommand.COMMAND_WORD: + return new ExportCsvCommandParser().parse(arguments); default: throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } diff --git a/src/main/java/seedu/address/logic/parser/ListCommandParser.java b/src/main/java/seedu/address/logic/parser/ListCommandParser.java new file mode 100644 index 00000000000..e3056b52a02 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ListCommandParser.java @@ -0,0 +1,35 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.DataTypeFlags.FLAG_APPLICANT; +import static seedu.address.commons.core.DataTypeFlags.FLAG_INTERVIEW; +import static seedu.address.commons.core.DataTypeFlags.FLAG_POSITION; + +import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.parser.applicants.ListApplicantCommandParser; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.logic.parser.interview.ListInterviewCommandParser; +import seedu.address.logic.parser.position.ListPositionCommandParser; + +public class ListCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments for data type flag and calls the respective + * ListXCommand according to the flag specified. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public ListCommand parse(String args) throws ParseException { + char flag = ArgumentTokenizer.getFlag(args.trim()); + String argsWithoutFlag = ArgumentTokenizer.removeFlag(args.trim()); + + if (flag == FLAG_APPLICANT) { + return new ListApplicantCommandParser().parse(argsWithoutFlag); + } else if (flag == FLAG_INTERVIEW) { + return new ListInterviewCommandParser().parse(argsWithoutFlag); + } else if (flag == FLAG_POSITION) { + return new ListPositionCommandParser().parse(argsWithoutFlag); + } + + return null; + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..903a198c2b6 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,17 +2,33 @@ import static java.util.Objects.requireNonNull; +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.ResolverStyle; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.Messages; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.FilterArgument; +import seedu.address.logic.FilterType; +import seedu.address.logic.HelpArgument; +import seedu.address.logic.SortArgument; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; +import seedu.address.model.applicant.Address; +import seedu.address.model.applicant.Age; +import seedu.address.model.applicant.Email; +import seedu.address.model.applicant.Gender; +import seedu.address.model.applicant.Name; +import seedu.address.model.applicant.Phone; +import seedu.address.model.position.Description; +import seedu.address.model.position.PositionName; +import seedu.address.model.position.PositionOpenings; +import seedu.address.model.position.Requirement; import seedu.address.model.tag.Tag; /** @@ -121,4 +137,181 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses a {@code String age} into a {@code age}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code age} is invalid. + */ + public static Age parseAge(String age) throws ParseException { + requireNonNull(age); + String trimmedAge = age.trim(); + if (!Age.isValidAge(trimmedAge)) { + throw new ParseException(Age.MESSAGE_CONSTRAINTS); + } + return new Age(trimmedAge); + } + + /** + * Parses a {@code String gender} into a {@code age}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code age} is invalid. + */ + public static Gender parseGender(String gender) throws ParseException { + requireNonNull(gender); + String trimmedGender = gender.trim(); + if (!Gender.isValidGender(trimmedGender)) { + throw new ParseException(Gender.MESSAGE_CONSTRAINTS); + } + return new Gender(trimmedGender); + } + + /** + * Parses a {@code String date} into an {@code LocalDateTime}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code date} is invalid. + */ + public static LocalDateTime parseDate(String date) throws ParseException { + requireNonNull(date); + String trimmedDate = date.trim(); + + // See whether date is valid + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm") + .withResolverStyle(ResolverStyle.STRICT); + LocalDateTime dateParsed = LocalDateTime.parse(date, formatter); + return dateParsed; + } catch (DateTimeException e) { + throw new ParseException(Messages.MESSAGE_INVALID_DATETIME); + } + + } + + /** + * Parses a {@code String positionName} into a {@code PositionName}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code positionName} is invalid. + */ + public static PositionName parsePositionName(String positionName) throws ParseException { + requireNonNull(positionName); + String trimmedName = positionName.trim(); + if (!PositionName.isValidPositionName(trimmedName)) { + throw new ParseException(PositionName.MESSAGE_CONSTRAINTS); + } + return new PositionName(trimmedName); + } + + /** + * Parses a {@code String description} into a {@code Description}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code description} is invalid. + */ + public static Description parseDescription(String description) throws ParseException { + requireNonNull(description); + String trimmedDescription = description.trim(); + if (!Description.isValidDescriptionText(trimmedDescription)) { + throw new ParseException(Description.MESSAGE_CONSTRAINTS); + } + return new Description(trimmedDescription); + } + + /** + * Parses a {@code String openings} into a {@code PositionOpenings}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code openings} is invalid. + */ + public static PositionOpenings parseOpenings(String openings) throws ParseException { + requireNonNull(openings); + String trimmedOpenings = openings.trim(); + if (!PositionOpenings.isValidNumber(openings)) { + throw new ParseException(PositionOpenings.MESSAGE_CONSTRAINTS); + } + return new PositionOpenings(trimmedOpenings); + } + + /** + * Parses a {@code String requirement} into a {@code Requirement}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code requirement} is invalid. + */ + public static Requirement parseRequirement(String requirement) throws ParseException { + requireNonNull(requirement); + String trimmedRequirement = requirement.trim(); + if (!Requirement.isValidRequirementText(trimmedRequirement)) { + throw new ParseException(Requirement.MESSAGE_CONSTRAINTS); + } + return new Requirement(trimmedRequirement); + } + + /** + * Parses {@code Collection requirements} into a {@code Set}. + */ + public static Set parseRequirements(Collection requirements) throws ParseException { + requireNonNull(requirements); + final Set requirementSet = new HashSet<>(); + for (String requirement : requirements) { + requirementSet.add(parseRequirement(requirement)); + } + return requirementSet; + } + + /** + * Parses a {@code String filterType} into a {@code FilterType}, along with the corresponding data type. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code filterType} is invalid for the {@code dataType}. + */ + public static FilterType parseFilterType(DataType dataType, String filterType) throws ParseException { + requireNonNull(filterType); + String trimmedFilterType = filterType.trim().toLowerCase(); + if (!FilterType.isValidFilterType(dataType, trimmedFilterType)) { + throw new ParseException(FilterType.MESSAGE_CONSTRAINTS); + } + return new FilterType(dataType, trimmedFilterType); + } + + /** + * Parses a {@code String filterArgument} into a {@code FilterArgument}. + * Leading and trailing whitespaces will be trimmed. + */ + public static FilterArgument parseFilterArgument(String filterArgument) throws ParseException { + requireNonNull(filterArgument); + if (filterArgument.trim().isEmpty()) { + throw new ParseException(FilterArgument.MESSAGE_CONSTRAINTS); + } + return new FilterArgument(filterArgument.trim()); + } + + /** + * Parses a {@code String sortArgument} into a {@code SortArgument}. + * Leading and trailing whitespaces will be trimmed, and argument will be converted to lower case. + */ + public static SortArgument parseSortArgument(String sortArgument) throws ParseException { + requireNonNull(sortArgument); + String trimmedSortArgument = sortArgument.trim().toLowerCase(); + if (!SortArgument.isValidSortArgument(sortArgument)) { + throw new ParseException(SortArgument.MESSAGE_CONSTRAINTS); + } + return new SortArgument(trimmedSortArgument); + } + + /** + * Parses a {@code String helpArgument} into a {@code HelpArgument}. + * All whitespaces will be trimmed, and argument will be converted to lower case. + */ + public static HelpArgument parseHelpArgument(String helpArgument) throws ParseException { + requireNonNull(helpArgument); + String trimmedHelpArgument = helpArgument.replaceAll("\\s+", "").toLowerCase(); + if (!HelpArgument.isValidHelpArgument(trimmedHelpArgument)) { + throw new ParseException(HelpArgument.COMMAND_NOT_FOUND_DESCRIPTION); + } + return new HelpArgument(trimmedHelpArgument); + } } diff --git a/src/main/java/seedu/address/logic/parser/applicants/AddApplicantCommandParser.java b/src/main/java/seedu/address/logic/parser/applicants/AddApplicantCommandParser.java new file mode 100644 index 00000000000..6d2b34874fa --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/applicants/AddApplicantCommandParser.java @@ -0,0 +1,71 @@ +package seedu.address.logic.parser.applicants; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_AGE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GENDER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.applicant.AddApplicantCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.applicant.Address; +import seedu.address.model.applicant.Age; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.applicant.Email; +import seedu.address.model.applicant.Gender; +import seedu.address.model.applicant.Name; +import seedu.address.model.applicant.Phone; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new AddApplicantCommand object + */ +public class AddApplicantCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddApplicantCommand + * and returns an AddApplicantCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddApplicantCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_AGE, PREFIX_ADDRESS, PREFIX_GENDER, PREFIX_PHONE, + PREFIX_EMAIL, PREFIX_TAG); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_AGE, PREFIX_ADDRESS, PREFIX_GENDER, PREFIX_PHONE, + PREFIX_EMAIL) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddApplicantCommand.MESSAGE_USAGE)); + } + + 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()); + Age age = ParserUtil.parseAge(argMultimap.getValue(PREFIX_AGE).get()); + Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + Gender gender = ParserUtil.parseGender(argMultimap.getValue(PREFIX_GENDER).get()); + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + + Applicant applicant = new Applicant(name, phone, email, age, address, gender, tagList); + + return new AddApplicantCommand(applicant); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/applicants/DeleteApplicantCommandParser.java b/src/main/java/seedu/address/logic/parser/applicants/DeleteApplicantCommandParser.java new file mode 100644 index 00000000000..dbed4eebc3a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/applicants/DeleteApplicantCommandParser.java @@ -0,0 +1,31 @@ +package seedu.address.logic.parser.applicants; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.applicant.DeleteApplicantCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteApplicantCommand object + */ +public class DeleteApplicantCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteApplicantCommand + * and returns a DeleteApplicantCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteApplicantCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteApplicantCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteApplicantCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/applicants/EditApplicantCommandParser.java b/src/main/java/seedu/address/logic/parser/applicants/EditApplicantCommandParser.java new file mode 100644 index 00000000000..0eef6546779 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/applicants/EditApplicantCommandParser.java @@ -0,0 +1,96 @@ +package seedu.address.logic.parser.applicants; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_AGE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_GENDER; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.applicant.EditApplicantCommand; +import seedu.address.logic.commands.applicant.EditApplicantCommand.EditApplicantDescriptor; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new EditApplicantCommand object + */ +public class EditApplicantCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditApplicantCommand + * and returns an EditApplicantCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditApplicantCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_AGE, PREFIX_ADDRESS, PREFIX_GENDER, PREFIX_PHONE, + PREFIX_EMAIL, PREFIX_TAG); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditApplicantCommand.MESSAGE_USAGE), pe); + } + + EditApplicantDescriptor editApplicantDescriptor = new EditApplicantDescriptor(); + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + editApplicantDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + } + if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { + editApplicantDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); + } + if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { + editApplicantDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + } + if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { + editApplicantDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + } + if (argMultimap.getValue(PREFIX_AGE).isPresent()) { + editApplicantDescriptor.setAge(ParserUtil.parseAge(argMultimap.getValue(PREFIX_AGE).get())); + } + if (argMultimap.getValue(PREFIX_GENDER).isPresent()) { + editApplicantDescriptor.setGender(ParserUtil.parseGender(argMultimap.getValue(PREFIX_GENDER).get())); + } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editApplicantDescriptor::setTags); + + if (!editApplicantDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditApplicantCommand.MESSAGE_NOT_EDITED); + } + + return new EditApplicantCommand(index, editApplicantDescriptor); + } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseTagsForEdit(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/applicants/ListApplicantCommandParser.java b/src/main/java/seedu/address/logic/parser/applicants/ListApplicantCommandParser.java new file mode 100644 index 00000000000..9ff15e592e4 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/applicants/ListApplicantCommandParser.java @@ -0,0 +1,86 @@ +package seedu.address.logic.parser.applicants; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_ARGUMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_TYPE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SORT_ARGUMENT; + +import seedu.address.commons.core.DataType; +import seedu.address.logic.FilterArgument; +import seedu.address.logic.FilterType; +import seedu.address.logic.SortArgument; +import seedu.address.logic.commands.applicant.ListApplicantCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.GenericListParser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.applicant.Gender; + +public class ListApplicantCommandParser extends GenericListParser { + + public static final String STATUS_REGEX = "available|hired"; + + public static final String MESSAGE_INVALID_STATUS = + "Applicant's status should only be available/hired (case-sensitive)"; + + @Override + public ListApplicantCommand returnFullList() { + return new ListApplicantCommand(); + } + + @Override + public ListApplicantCommand parseFilterAndSort(ArgumentMultimap args) throws ParseException { + FilterType filterType = + ParserUtil.parseFilterType(DataType.APPLICANT, args.getValue(PREFIX_FILTER_TYPE).get()); + FilterArgument filterArgument = + ParserUtil.parseFilterArgument(args.getValue(PREFIX_FILTER_ARGUMENT).get()); + SortArgument sortArgument = + ParserUtil.parseSortArgument(args.getValue(PREFIX_SORT_ARGUMENT).get()); + + checkFilterTypeArgument(filterType, filterArgument); + + return new ListApplicantCommand(filterType, filterArgument, sortArgument); + } + + /** + * Parses the given {@code String} of arguments in the context of performing sort feature + * and returns an ListApplicantCommand object for execution. + * @param args The input arguments string + * @return ListApplicantCommand object with respective filter type and filter argument for execution + */ + @Override + public ListApplicantCommand parseSort(ArgumentMultimap args) throws ParseException { + SortArgument sortArgument = + ParserUtil.parseSortArgument(args.getValue(PREFIX_SORT_ARGUMENT).get()); + + return new ListApplicantCommand(sortArgument); + } + + /** + * Parses the given {@code String} of arguments in the context of performing filter feature + * and returns an ListApplicantCommand object for execution. + * @param args The input arguments string + * @return ListApplicantCommand object with respective filter type and filter argument for execution + */ + @Override + public ListApplicantCommand parseFilter(ArgumentMultimap args) throws ParseException { + FilterType filterType = + ParserUtil.parseFilterType(DataType.APPLICANT, args.getValue(PREFIX_FILTER_TYPE).get()); + FilterArgument filterArgument = + ParserUtil.parseFilterArgument(args.getValue(PREFIX_FILTER_ARGUMENT).get()); + + checkFilterTypeArgument(filterType, filterArgument); + + return new ListApplicantCommand(filterType, filterArgument); + } + + /** + * Checks if the given {@code FilterArgument} if valid for the given {@code FilterType}. + */ + public void checkFilterTypeArgument(FilterType filterType, FilterArgument filterArgument) throws ParseException { + if (filterType.type.equals("gender") && !Gender.isValidGender(filterArgument.toString())) { + throw new ParseException(Gender.MESSAGE_CONSTRAINTS); + } else if (filterType.type.equals("status") && !filterArgument.toString().matches(STATUS_REGEX)) { + throw new ParseException(MESSAGE_INVALID_STATUS); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/help/HelpCommandParser.java b/src/main/java/seedu/address/logic/parser/help/HelpCommandParser.java new file mode 100644 index 00000000000..0130349bc9f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/help/HelpCommandParser.java @@ -0,0 +1,20 @@ +package seedu.address.logic.parser.help; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.HelpArgument; +import seedu.address.logic.commands.help.DetailHelpCommand; +import seedu.address.logic.commands.help.HelpCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + + +public class HelpCommandParser implements Parser { + @Override + public HelpCommand parse(String userInput) throws ParseException { + requireNonNull(userInput); + HelpArgument helpArgument = ParserUtil.parseHelpArgument(userInput); + return new DetailHelpCommand(helpArgument); + } +} diff --git a/src/main/java/seedu/address/logic/parser/interview/AcceptInterviewCommandParser.java b/src/main/java/seedu/address/logic/parser/interview/AcceptInterviewCommandParser.java new file mode 100644 index 00000000000..9313a7fd707 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/interview/AcceptInterviewCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser.interview; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.interview.AcceptInterviewCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AcceptInterviewCommand object + */ +public class AcceptInterviewCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AcceptInterviewCommand + * and returns a AcceptInterviewCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AcceptInterviewCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new AcceptInterviewCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AcceptInterviewCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/interview/AddInterviewCommandParser.java b/src/main/java/seedu/address/logic/parser/interview/AddInterviewCommandParser.java new file mode 100644 index 00000000000..ae89a334434 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/interview/AddInterviewCommandParser.java @@ -0,0 +1,66 @@ +package seedu.address.logic.parser.interview; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; + +import java.time.LocalDateTime; +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.interview.AddInterviewCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; + + +/** + * Parses input arguments and creates a new AddInterviewCommand object + */ +public class AddInterviewCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddInterviewCommand + * and returns an AddInterviewCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddInterviewCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_DATE, PREFIX_POSITION); + + Index applicantIndex; + + // Find applicant index, to be converted into actual applicant in AddInterviewCommand. This is because we need + // the model class to help us match the index to the actual applicant + try { + applicantIndex = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddInterviewCommand.MESSAGE_USAGE), pe); + } + + if (!arePrefixesPresent(argMultimap, PREFIX_DATE, PREFIX_POSITION)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddInterviewCommand.MESSAGE_USAGE)); + } + + // Can find actual date and do error checking here + LocalDateTime date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + + // Find position index, to be converted into actual position in AddInterviewCommand. This is because we need + // the model class to help us match the index to the actual applicant + Index positionIndex = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_POSITION).get()); + + return new AddInterviewCommand(applicantIndex, date, positionIndex); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/interview/DeleteInterviewCommandParser.java b/src/main/java/seedu/address/logic/parser/interview/DeleteInterviewCommandParser.java new file mode 100644 index 00000000000..6a6a9ca3463 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/interview/DeleteInterviewCommandParser.java @@ -0,0 +1,27 @@ +package seedu.address.logic.parser.interview; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.interview.DeleteInterviewCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +public class DeleteInterviewCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteInterviewCommand + * and returns a DeleteInterviewCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteInterviewCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteInterviewCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteInterviewCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/interview/EditInterviewCommandParser.java b/src/main/java/seedu/address/logic/parser/interview/EditInterviewCommandParser.java new file mode 100644 index 00000000000..2442b9da9a5 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/interview/EditInterviewCommandParser.java @@ -0,0 +1,60 @@ +package seedu.address.logic.parser.interview; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPLICANT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.interview.EditInterviewCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +public class EditInterviewCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditInterviewCommand + * and returns an EditInterviewCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public EditInterviewCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_APPLICANT, PREFIX_DATE, PREFIX_POSITION); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditInterviewCommand.MESSAGE_USAGE), pe); + } + + EditInterviewCommand.EditInterviewDescriptor editInterviewDescriptor = + new EditInterviewCommand.EditInterviewDescriptor(); + if (argMultimap.getValue(PREFIX_APPLICANT).isPresent()) { + editInterviewDescriptor.setApplicantIndex(ParserUtil + .parseIndex(argMultimap.getValue(PREFIX_APPLICANT).get())); + } + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + editInterviewDescriptor.setDate(ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get())); + } + if (argMultimap.getValue(PREFIX_POSITION).isPresent()) { + editInterviewDescriptor.setPositionIndex(ParserUtil + .parseIndex(argMultimap.getValue(PREFIX_POSITION).get())); + } + + if (!editInterviewDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditInterviewCommand.MESSAGE_NOT_EDITED); + } + + return new EditInterviewCommand(index, editInterviewDescriptor); + } +} diff --git a/src/main/java/seedu/address/logic/parser/interview/FailInterviewCommandParser.java b/src/main/java/seedu/address/logic/parser/interview/FailInterviewCommandParser.java new file mode 100644 index 00000000000..08359eb0a7f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/interview/FailInterviewCommandParser.java @@ -0,0 +1,26 @@ +package seedu.address.logic.parser.interview; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.interview.FailInterviewCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +public class FailInterviewCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the PassInterviewCommand + * and returns a PassInterviewCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FailInterviewCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new FailInterviewCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FailInterviewCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/interview/ListInterviewCommandParser.java b/src/main/java/seedu/address/logic/parser/interview/ListInterviewCommandParser.java new file mode 100644 index 00000000000..98a5c774ca4 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/interview/ListInterviewCommandParser.java @@ -0,0 +1,95 @@ +package seedu.address.logic.parser.interview; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_ARGUMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_TYPE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SORT_ARGUMENT; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +import seedu.address.commons.core.DataType; +import seedu.address.commons.core.Messages; +import seedu.address.logic.FilterArgument; +import seedu.address.logic.FilterType; +import seedu.address.logic.SortArgument; +import seedu.address.logic.commands.interview.ListInterviewCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.GenericListParser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +public class ListInterviewCommandParser extends GenericListParser { + + public static final String STATUS_REGEX = "pending|passed|failed|accepted|rejected"; + + public static final String MESSAGE_INVALID_STATUS = + "Interview status should only be pending/passed/failed/accepted/rejected (case-sensitive)"; + + @Override + public ListInterviewCommand returnFullList() { + return new ListInterviewCommand(); + } + + @Override + public ListInterviewCommand parseFilterAndSort(ArgumentMultimap args) throws ParseException { + FilterType filterType = + ParserUtil.parseFilterType(DataType.INTERVIEW, args.getValue(PREFIX_FILTER_TYPE).get()); + FilterArgument filterArgument = + ParserUtil.parseFilterArgument(args.getValue(PREFIX_FILTER_ARGUMENT).get()); + SortArgument sortArgument = + ParserUtil.parseSortArgument(args.getValue(PREFIX_SORT_ARGUMENT).get()); + + checkFilterTypeArgument(filterType, filterArgument); + + return new ListInterviewCommand(filterType, filterArgument, sortArgument); + } + + /** + * Parses the given {@code String} of arguments in the context of performing sort feature + * and returns an ListInterviewCommand object for execution. + * @param args The input arguments string + * @return ListInterviewCommand object with respective sort argument for execution + * @throws ParseException if the user input does not conform the expected sort format + */ + @Override + public ListInterviewCommand parseSort(ArgumentMultimap args) throws ParseException { + SortArgument sortArgument = + ParserUtil.parseSortArgument(args.getValue(PREFIX_SORT_ARGUMENT).get()); + + return new ListInterviewCommand(sortArgument); + } + + /** + * Parses the given {@code String} of arguments in the context of performing filter feature + * and returns an ListInterviewCommand object for execution. + * @param args The input arguments string + * @return ListInterviewCommand object with respective filter type and filter argument for execution + * @throws ParseException if the user input does not conform the expected filter format + */ + @Override + public ListInterviewCommand parseFilter(ArgumentMultimap args) throws ParseException { + FilterType filterType = + ParserUtil.parseFilterType(DataType.INTERVIEW, args.getValue(PREFIX_FILTER_TYPE).get()); + FilterArgument filterArgument = + ParserUtil.parseFilterArgument(args.getValue(PREFIX_FILTER_ARGUMENT).get()); + + checkFilterTypeArgument(filterType, filterArgument); + + return new ListInterviewCommand(filterType, filterArgument); + } + + /** + * Checks if the given {@code FilterArgument} if valid for the given {@code FilterType}. + */ + public void checkFilterTypeArgument(FilterType filterType, FilterArgument filterArgument) throws ParseException { + if (filterType.type.equals("date")) { + try { + LocalDate.parse(filterArgument.toString()); + } catch (DateTimeParseException e) { + throw new ParseException(Messages.MESSAGE_INVALID_DATE); + } + } else if (filterType.type.equals("status") && !filterArgument.toString().matches(STATUS_REGEX)) { + throw new ParseException(MESSAGE_INVALID_STATUS); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/interview/PassInterviewCommandParser.java b/src/main/java/seedu/address/logic/parser/interview/PassInterviewCommandParser.java new file mode 100644 index 00000000000..d8ac8197d4e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/interview/PassInterviewCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser.interview; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.interview.PassInterviewCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new PassInterviewCommand object + */ +public class PassInterviewCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the PassInterviewCommand + * and returns a PassInterviewCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public PassInterviewCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new PassInterviewCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, PassInterviewCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/interview/RejectInterviewCommandParser.java b/src/main/java/seedu/address/logic/parser/interview/RejectInterviewCommandParser.java new file mode 100644 index 00000000000..45e29a9b361 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/interview/RejectInterviewCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser.interview; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.interview.RejectInterviewCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AcceptInterviewCommand object + */ +public class RejectInterviewCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the RejectInterviewCommand + * and returns a RejectInterviewCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public RejectInterviewCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new RejectInterviewCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RejectInterviewCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/position/AddPositionCommandParser.java b/src/main/java/seedu/address/logic/parser/position/AddPositionCommandParser.java new file mode 100644 index 00000000000..a1dd1e4ae96 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/position/AddPositionCommandParser.java @@ -0,0 +1,61 @@ +package seedu.address.logic.parser.position; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NUM_OPENINGS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REQUIREMENT; + +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.position.AddPositionCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.position.Description; +import seedu.address.model.position.Position; +import seedu.address.model.position.PositionName; +import seedu.address.model.position.PositionOpenings; +import seedu.address.model.position.Requirement; + +public class AddPositionCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddPositionCommand + * and returns an AddPositionCommand object for execution. + * + * @throws ParseException if the user input does not conform to the expected format + */ + @Override + public AddPositionCommand parse(String args) throws ParseException { + ArgumentMultimap argMultiMap = + ArgumentTokenizer.tokenize(args, PREFIX_POSITION, PREFIX_NUM_OPENINGS, PREFIX_DESCRIPTION, + PREFIX_REQUIREMENT); + + if (!arePrefixesPresent(argMultiMap, PREFIX_POSITION, PREFIX_NUM_OPENINGS, PREFIX_DESCRIPTION) + || !argMultiMap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddPositionCommand.MESSAGE_USAGE)); + } + + PositionName positionName = ParserUtil.parsePositionName(argMultiMap.getValue(PREFIX_POSITION).get()); + PositionOpenings positionOpenings = ParserUtil.parseOpenings(argMultiMap.getValue(PREFIX_NUM_OPENINGS).get()); + Description description = ParserUtil.parseDescription(argMultiMap.getValue(PREFIX_DESCRIPTION).get()); + Set requirements = ParserUtil.parseRequirements(argMultiMap.getAllValues(PREFIX_REQUIREMENT)); + + Position position = new Position(positionName, description, positionOpenings, requirements); + + return new AddPositionCommand(position); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/position/DeletePositionCommandParser.java b/src/main/java/seedu/address/logic/parser/position/DeletePositionCommandParser.java new file mode 100644 index 00000000000..c66ce7c5a03 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/position/DeletePositionCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser.position; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.position.DeletePositionCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and Creates a new DeletePositionCommand object + */ +public class DeletePositionCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the DeletePositionCommand + * and returns a DeletePositionCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public DeletePositionCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeletePositionCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeletePositionCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/position/EditPositionCommandParser.java b/src/main/java/seedu/address/logic/parser/position/EditPositionCommandParser.java new file mode 100644 index 00000000000..e620fe6bb3c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/position/EditPositionCommandParser.java @@ -0,0 +1,89 @@ +package seedu.address.logic.parser.position; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NUM_OPENINGS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REQUIREMENT; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.position.EditPositionCommand; +import seedu.address.logic.commands.position.EditPositionCommand.EditPositionDescriptor; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.position.Requirement; + +public class EditPositionCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditPositionCommand + * and returns an EditPositionCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public EditPositionCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_POSITION, PREFIX_NUM_OPENINGS, PREFIX_DESCRIPTION, + PREFIX_REQUIREMENT); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditPositionCommand.MESSAGE_USAGE), pe); + } + + EditPositionDescriptor editPositionDescriptor = new EditPositionDescriptor(); + if (argMultimap.getValue(PREFIX_POSITION).isPresent()) { + editPositionDescriptor.setPositionName(ParserUtil + .parsePositionName(argMultimap.getValue(PREFIX_POSITION).get())); + } + if (argMultimap.getValue(PREFIX_DESCRIPTION).isPresent()) { + editPositionDescriptor.setDescription(ParserUtil + .parseDescription(argMultimap.getValue(PREFIX_DESCRIPTION).get())); + } + if (argMultimap.getValue(PREFIX_NUM_OPENINGS).isPresent()) { + editPositionDescriptor.setPositionOpenings(ParserUtil + .parseOpenings(argMultimap.getValue(PREFIX_NUM_OPENINGS).get())); + } + parseRequirementsForEdit(argMultimap.getAllValues(PREFIX_REQUIREMENT)) + .ifPresent(editPositionDescriptor::setRequirements); + + if (!editPositionDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditPositionCommand.MESSAGE_NOT_EDITED); + } + + return new EditPositionCommand(index, editPositionDescriptor); + } + + /** + * Parses {@code Collection requirements} into a {@code Set} if {@code requirements} + * is non-empty. + * If {@code requirements} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero requirements. + */ + private Optional> parseRequirementsForEdit(Collection requirements) + throws ParseException { + assert requirements != null; + + if (requirements.isEmpty()) { + return Optional.empty(); + } + Collection requirementSet = requirements.size() == 1 && requirements.contains("") + ? Collections.emptySet() : requirements; + return Optional.of(ParserUtil.parseRequirements(requirementSet)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/position/ListPositionCommandParser.java b/src/main/java/seedu/address/logic/parser/position/ListPositionCommandParser.java new file mode 100644 index 00000000000..9588a63b8e1 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/position/ListPositionCommandParser.java @@ -0,0 +1,65 @@ +package seedu.address.logic.parser.position; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_ARGUMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILTER_TYPE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SORT_ARGUMENT; + +import seedu.address.commons.core.DataType; +import seedu.address.logic.FilterArgument; +import seedu.address.logic.FilterType; +import seedu.address.logic.SortArgument; +import seedu.address.logic.commands.position.ListPositionCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.GenericListParser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +public class ListPositionCommandParser extends GenericListParser { + + @Override + public ListPositionCommand returnFullList() { + return new ListPositionCommand(); + } + + @Override + public ListPositionCommand parseFilterAndSort(ArgumentMultimap args) throws ParseException { + FilterType filterType = + ParserUtil.parseFilterType(DataType.POSITION, args.getValue(PREFIX_FILTER_TYPE).get()); + FilterArgument filterArgument = + ParserUtil.parseFilterArgument(args.getValue(PREFIX_FILTER_ARGUMENT).get()); + SortArgument sortArgument = + ParserUtil.parseSortArgument(args.getValue(PREFIX_SORT_ARGUMENT).get()); + + return new ListPositionCommand(filterType, filterArgument, sortArgument); + } + + /** + * Parses the given {@code String} of arguments in the context of performing sort feature + * and returns an ListPositionCommand object for execution. + * @param args The input arguments string + * @return ListPositionCommand object with respective sort argument for execution + * @throws ParseException if the user input does not conform the expected sort format + */ + public ListPositionCommand parseSort(ArgumentMultimap args) throws ParseException { + SortArgument sortArgument = + ParserUtil.parseSortArgument(args.getValue(PREFIX_SORT_ARGUMENT).get()); + + return new ListPositionCommand(sortArgument); + } + + /** + * Parses the given {@code String} of arguments in the context of performing sort feature + * and returns an ListPositionCommand object for execution. + * @param args The input arguments string + * @return ListPositionCommand object with respective sort argument for execution + * @throws ParseException if the user input does not conform the expected sort format + */ + public ListPositionCommand parseFilter(ArgumentMultimap args) throws ParseException { + FilterType filterType = + ParserUtil.parseFilterType(DataType.POSITION, args.getValue(PREFIX_FILTER_TYPE).get()); + FilterArgument filterArgument = + ParserUtil.parseFilterArgument(args.getValue(PREFIX_FILTER_ARGUMENT).get()); + + return new ListPositionCommand(filterType, filterArgument); + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java deleted file mode 100644 index 1a943a0781a..00000000000 --- a/src/main/java/seedu/address/model/AddressBook.java +++ /dev/null @@ -1,120 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; - -/** - * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) - */ -public class AddressBook implements ReadOnlyAddressBook { - - private final UniquePersonList persons; - - /* - * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication - * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html - * - * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication - * among constructors. - */ - { - persons = new UniquePersonList(); - } - - public AddressBook() {} - - /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} - */ - public AddressBook(ReadOnlyAddressBook toBeCopied) { - this(); - resetData(toBeCopied); - } - - //// list overwrite operations - - /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - this.persons.setPersons(persons); - } - - /** - * Resets the existing data of this {@code AddressBook} with {@code newData}. - */ - public void resetData(ReadOnlyAddressBook newData) { - requireNonNull(newData); - - setPersons(newData.getPersonList()); - } - - //// person-level operations - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); - } - - /** - * Adds a person to the address book. - * The person must not already exist in the address book. - */ - public void addPerson(Person p) { - persons.add(p); - } - - /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); - - persons.setPerson(target, editedPerson); - } - - /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. - */ - public void removePerson(Person key) { - persons.remove(key); - } - - //// util methods - - @Override - public String toString() { - return persons.asUnmodifiableObservableList().size() + " persons"; - // TODO: refine later - } - - @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddressBook // instanceof handles nulls - && persons.equals(((AddressBook) other).persons)); - } - - @Override - public int hashCode() { - return persons.hashCode(); - } -} diff --git a/src/main/java/seedu/address/model/HireLah.java b/src/main/java/seedu/address/model/HireLah.java new file mode 100644 index 00000000000..1a191ffa656 --- /dev/null +++ b/src/main/java/seedu/address/model/HireLah.java @@ -0,0 +1,338 @@ +package seedu.address.model; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import javafx.collections.ObservableList; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.applicant.Email; +import seedu.address.model.applicant.Phone; +import seedu.address.model.applicant.UniqueApplicantList; +import seedu.address.model.interview.Interview; +import seedu.address.model.interview.UniqueInterviewList; +import seedu.address.model.position.Position; +import seedu.address.model.position.UniquePositionList; + +/** + * Wraps all data at the address-book level + * Duplicates are not allowed (by .isSameApplicant comparison) + */ +public class HireLah implements ReadOnlyHireLah { + + private final UniqueApplicantList applicants; + private final UniqueInterviewList interviews; + private final UniquePositionList positions; + + /* + * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication + * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html + * + * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication + * among constructors. + */ + { + applicants = new UniqueApplicantList(); + interviews = new UniqueInterviewList(); + positions = new UniquePositionList(); + } + + public HireLah() {} + + /** + * Creates an HireLah using the Persons in the {@code toBeCopied} + */ + public HireLah(ReadOnlyHireLah toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the applicant list with {@code applicants}. + * {@code applicants} must not contain duplicate applicants. + */ + public void setApplicants(List applicants) { + this.applicants.setApplicants(applicants); + } + + /** + * Replaces the contents of the position list with {@code positions}. + * {@code positions} must not contain duplicate positions. + */ + public void setPositions(List positions) { + this.positions.setPositions(positions); + } + + /** + * Replaces the contents of the interview list with {@code interviews}. + * {@code interviews} must not contain duplicate interviews. + */ + public void setInterviews(List interviews) { + this.interviews.setInterviews(interviews); + } + + /** + * Resets the existing data of this {@code HireLah} with {@code newData}. + */ + public void resetData(ReadOnlyHireLah newData) { + requireNonNull(newData); + + setApplicants(newData.getApplicantList()); + setPositions(newData.getPositionList()); + setInterviews(newData.getInterviewList()); + + } + + //// applicant-level operations + + /** + * Returns true if an applicant with the same identity as {@code applicant} exists in the address book. + */ + public boolean hasApplicant(Applicant applicant) { + requireNonNull(applicant); + return applicants.contains(applicant); + } + + /** + * Returns the {@code Applicant} with the {@code email} provided if exists; or null if no such applicant. + */ + public Applicant getApplicantWithEmail(Email email) { + requireNonNull(email); + return applicants.getApplicantWithEmail(email); + } + + /** + * Returns the {@code Applicant} with the {@code phone} provided if exists; or null if no such applicant. + */ + public Applicant getApplicantWithPhone(Phone phone) { + requireNonNull(phone); + return applicants.getApplicantWithPhone(phone); + } + + /** + * Adds an applicant to the address book. + * The applicant must not already exist in the address book. + */ + public void addApplicant(Applicant p) { + applicants.add(p); + } + + /** + * Replaces the given applicant {@code target} in the list with {@code editedApplicant}. + * {@code target} must exist in the address book. + * The applicant identity of {@code editedApplicant} must not be the same as another existing applicant + * in the address book. + */ + public void setApplicant(Applicant target, Applicant editedApplicant) { + requireNonNull(editedApplicant); + + applicants.setApplicant(target, editedApplicant); + } + + /** + * Removes {@code key} from this {@code HireLah}. + * {@code key} must exist in the address book. + */ + public void removeApplicant(Applicant key) { + applicants.remove(key); + } + + /** + * Sorts list of applicant using comparator + */ + public void sortApplicant(Comparator comparator) { + applicants.sort(comparator); + } + + //// interview-level operations + + /** + * Returns true if an interview that is the same as {@code interview} exists in the address book. + */ + public boolean hasInterview(Interview i) { + requireNonNull(i); + return interviews.contains(i); + } + + /** + * Returns true if an interview has a conflict in timing as {@code interview} exists in the address book. + */ + public boolean hasConflictingInterview(Interview i) { + requireNonNull(i); + return interviews.containsConflict(i); + } + + + /** + * Removes {@code key} from this {@code HireLah}. + * {@code key} must exist in the address book. + */ + public void removeInterview(Interview key) { + interviews.remove(key); + } + + /** + * Adds an interview to the address book. + * The interview must not already exist in the address book. + */ + public void addInterview(Interview i) { + interviews.add(i); + } + + /** + * Returns interview(s) which are for the specified applicant. + */ + public ArrayList getApplicantsInterviews(Applicant applicant) { + return interviews.getApplicantsInterviews(applicant); + } + + /** + * Returns interview(s) which are for the specified position. + */ + public ArrayList getPositionsInterview(Position position) { + return interviews.getPositionsInterview(position); + } + + /** + * Checks if the specified applicant has an interview for the specified position. + */ + public boolean isSameApplicantPosition(Applicant applicant, Position position) { + return interviews.isSameApplicantPosition(applicant, position); + } + + /** + * Replaces the given interview {@code target} with {@code editedInterview}. + * {@code target} must exist in HireLah. + * The interview identity of {@code editedInterview} must not be the same as another existing interview + * in HireLah. + */ + public void setInterview(Interview target, Interview editedInterview) { + requireNonNull(editedInterview); + interviews.setInterview(target, editedInterview); + } + + /** + * Sorts list of interview using comparator + */ + public void sortInterview(Comparator comparator) { + requireNonNull(comparator); + interviews.sort(comparator); + } + + //// position-level operations + + /** + * Returns true if a position that is the same as {@code position} exists in the address book. + */ + public boolean hasPosition(Position position) { + requireNonNull(position); + return positions.contains(position); + } + + /** + * Adds a position to the address book. + * The position must not already exist in the address book. + */ + public void addPosition(Position position) { + positions.add(position); + } + + /** + * Removes {@code key} from this {@code HireLah}. + * {@code key} must exist in the address book. + */ + public void removePosition(Position key) { + positions.remove(key); + } + + /** + * Updates all old instances of {@code positionToBeUpdated} with {@code newPosition}. + * Existence of {@code positionToBeUpdated} and uniqueness of {@code newPosition} will be checked at + * {@link #setPosition(Position, Position)}. + */ + public void updatePosition(Position positionToBeUpdated, Position newPosition) { + requireAllNonNull(positionToBeUpdated, newPosition); + setPosition(positionToBeUpdated, newPosition); + interviews.updatePositions(positionToBeUpdated, newPosition); + } + + /** + * Updates all old instances of {@code applicantToBeUpdated} with {@code newApplicant}. + * Existence of {@code applicantToBeUpdated} and uniqueness of {@code newApplicant} will be checked at + * {@link #setApplicant(Applicant, Applicant)}. + */ + public void updateApplicant(Applicant applicantToBeUpdated, Applicant newApplicant) { + requireAllNonNull(applicantToBeUpdated, newApplicant); + setApplicant(applicantToBeUpdated, newApplicant); + interviews.updateApplicants(applicantToBeUpdated, newApplicant); + } + + /** + * Replaces the given position {@code target} in the list with {@code editedPosition}. + * {@code target} must exist in the address book. + * The position identity of {@code editedPosition} must not be the same as another existing position + * in the address book. + */ + public void setPosition(Position target, Position editedPosition) { + requireNonNull(editedPosition); + + positions.setPosition(target, editedPosition); + } + + /** + * Sorts list of interview using comparator + */ + public void sortPosition(Comparator comparator) { + requireNonNull(comparator); + positions.sort(comparator); + } + //// util methods + + @Override + public String toString() { + return applicants.asUnmodifiableObservableList().size() + " persons"; + // TODO: refine later + } + + @Override + public ObservableList getApplicantList() { + return applicants.asUnmodifiableObservableList(); + } + + @Override + public ObservableList getInterviewList() { + return interviews.asUnmodifiableObservableList(); + } + + @Override + public ObservableList getPositionList() { + return positions.asUnmodifiableObservableList(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof HireLah // instanceof handles nulls + && applicants.equals(((HireLah) other).applicants) + && interviews.equals(((HireLah) other).interviews) + && positions.equals(((HireLah) other).positions)); + } + + @Override + public int hashCode() { + return applicants.hashCode(); + } + + public Applicant getApplicantUsingStorage(Applicant interviewApplicant) { + return applicants.getApplicant(interviewApplicant); + } + + public Position getPositionUsingStorage(Position interviewPosition) { + return positions.getPosition(interviewPosition); + } +} diff --git a/src/main/java/seedu/address/model/ImmutableCounter.java b/src/main/java/seedu/address/model/ImmutableCounter.java new file mode 100644 index 00000000000..0393473916f --- /dev/null +++ b/src/main/java/seedu/address/model/ImmutableCounter.java @@ -0,0 +1,21 @@ +package seedu.address.model; + +/** + * API for an object that keeps track of an internal counter. + */ +public interface ImmutableCounter { + /** + * Increases an internal counter. + */ + ImmutableCounter increment(); + + /** + * Decreases an internal country. + */ + ImmutableCounter decrement(); + + /** + * Returns an integer that represents the value of the current state of the counter. + */ + Integer getCount(); +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..6cd04f90bff 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,18 +1,28 @@ package seedu.address.model; +import java.io.FileNotFoundException; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; +import seedu.address.commons.exceptions.ExportCsvOpenException; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.applicant.Email; +import seedu.address.model.applicant.Phone; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; /** * The API of the Model component. */ public interface Model { /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + Predicate PREDICATE_SHOW_ALL_APPLICANTS = unused -> true; + Predicate PREDICATE_SHOW_ALL_INTERVIEWS = unused -> true; + Predicate PREDICATE_SHOW_ALL_POSITIONS = unused -> true; /** * Replaces user prefs data with the data in {@code userPrefs}. @@ -35,53 +45,188 @@ public interface Model { void setGuiSettings(GuiSettings guiSettings); /** - * Returns the user prefs' address book file path. + * Returns the user prefs' HireLah file path. */ - Path getAddressBookFilePath(); + Path getHireLahFilePath(); /** - * Sets the user prefs' address book file path. + * Sets the user prefs' HireLah file path. */ - void setAddressBookFilePath(Path addressBookFilePath); + void setHireLahFilePath(Path addressBookFilePath); /** - * Replaces address book data with the data in {@code addressBook}. + * Replaces address book data with the data in {@code hireLah}. */ - void setAddressBook(ReadOnlyAddressBook addressBook); + void setHireLah(ReadOnlyHireLah hireLah); - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); + /** Returns the HireLah */ + ReadOnlyHireLah getHireLah(); /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a applicant with the same identity as {@code applicant} exists in the address book. */ - boolean hasPerson(Person person); + boolean hasApplicant(Applicant applicant); /** - * Deletes the given person. - * The person must exist in the address book. + * Returns the {@code Applicant} with the {@code email} provided if exists; or null if no such applicant. */ - void deletePerson(Person target); + Applicant getApplicantWithEmail(Email email); /** - * Adds the given person. - * {@code person} must not already exist in the address book. + * Returns the {@code Applicant} with the {@code phone} provided if exists; or null if no such applicant. */ - void addPerson(Person person); + Applicant getApplicantWithPhone(Phone phone); /** - * Replaces the given person {@code target} with {@code editedPerson}. + * Deletes the given applicant. + * The applicant must exist in the address book. + */ + void deleteApplicant(Applicant target); + + /** + * Adds the given applicant. + * {@code applicant} must not already exist in the address book. + */ + void addApplicant(Applicant applicant); + + /** + * Replaces the given applicant {@code target} with {@code editedApplicant}. * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * The applicant identity of {@code editedApplicant} must not be the same as another existing applicant + * in the address book. + */ + void setApplicant(Applicant target, Applicant editedApplicant); + + /** Returns an unmodifiable view of the filtered applicant list */ + ObservableList getFilteredApplicantList(); + + /** + * Updates the filter of the filtered applicant list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredApplicantList(Predicate predicate); + + /** + * Returns true if an interview already exists in the address book. + */ + boolean hasInterview(Interview interview); + + /** + * Returns true if an applicant already has an interview for that timeslot. + */ + boolean hasConflictingInterview(Interview interview); + + /** + * Deletes the given interview. + * The interview must exist in the address book. + */ + void deleteInterview(Interview target); + + /** + * Adds the given interview. + * {@code interview} must not already exist in HireLah. + */ + void addInterview(Interview interview); + + + /** + * Replaces the given interview {@code target} with {@code editedInterview}. + * {@code target} must exist in the address book. + * The interview identity of {@code editedInterview} must not be the same as another existing interview + * in the address book. + */ + void setInterview(Interview target, Interview editedInterview); + + /** Returns an unmodifiable view of the filtered interview list */ + ObservableList getFilteredInterviewList(); + + /** + * Updates the filter of the filtered interview list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredInterviewList(Predicate predicate); + + /** + * Returns interview(s) which are for the specified applicant. */ - void setPerson(Person target, Person editedPerson); + ArrayList getApplicantsInterviews(Applicant applicant); - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); + /** + * Returns interview(s) which are for the specified position. + */ + ArrayList getPositionsInterviews(Position position); /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. + * Checks if the specified applicant has an interview for the specified position. + */ + boolean isSameApplicantPosition(Applicant applicant, Position position); + + /** + * Updates the filter of the filtered position list to filter by the given {@code predicate}. + * * @throws NullPointerException if {@code predicate} is null. */ - void updateFilteredPersonList(Predicate predicate); + void updateFilteredPositionList(Predicate predicate); + + /** + * Deletes the given position. + * The position must exist in the address book. + */ + void deletePosition(Position target); + + /** + * Returns true if a position already exists in the address book. + */ + boolean hasPosition(Position position); + + /** + * Adds the given position. + * {@code position} must not already exist in the address book. + */ + void addPosition(Position position); + + /** + * Replaces the given position {@code target} with {@code editedPosition}. + * {@code target} must exist in the address book. + * The position identity of {@code editedPosition} must not be the same as another existing position + * in the address book. + */ + void setPosition(Position target, Position editedPosition); + + /** + * Replaces all instances of {@code positionToBeUpdated} with {@code newPosition}. + * {@code positionToBeUpdated} must exist in the address book. + * The position identity of {@code newPosition} must not be the same as another existing position + * in the address book. + */ + void updatePosition(Position positionToBeUpdated, Position newPosition); + + /** + * Replaces all instances of {@code applicantToBeUpdated} with {@code newApplicant}. + * {@code applicantToBeUpdated} must exist in the address book. + * The applicant identity of {@code newApplicant} must not be the same as another existing applicant + * in the address book. + */ + void updateApplicant(Applicant applicantToBeUpdated, Applicant newApplicant); + + /** Returns an unmodifiable view of the filtered position list */ + ObservableList getFilteredPositionList(); + + void updateSortApplicantList(Comparator comparator); + + void updateSortInterviewList(Comparator comparator); + + void updateSortPositionList(Comparator comparator); + + void updateFilterAndSortApplicantList(Predicate predicate, Comparator comparator); + + void updateFilterAndSortInterviewList(Predicate predicate, Comparator comparator); + + void updateFilterAndSortPositionList(Predicate predicate, Comparator comparator); + + void exportCsvApplicant() throws FileNotFoundException, ExportCsvOpenException; + + void exportCsvInterview() throws FileNotFoundException, ExportCsvOpenException; + + void exportCsvPosition() throws FileNotFoundException, ExportCsvOpenException; } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 86c1df298d7..ec9240bfa3b 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -3,41 +3,65 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintWriter; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; import java.util.function.Predicate; import java.util.logging.Logger; +import java.util.stream.Stream; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; +import seedu.address.commons.exceptions.ExportCsvOpenException; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.applicant.Email; +import seedu.address.model.applicant.Phone; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; /** * Represents the in-memory model of the address book data. */ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + private static final String EXPORT_CSV_FOLDER = "export_csv"; + private static final String APPLICANT_CSV_FILE = EXPORT_CSV_FOLDER + File.separator + "applicant.csv"; + private static final String INTERVIEW_CSV_FILE = EXPORT_CSV_FOLDER + File.separator + "interview.csv"; + private static final String POSITION_CSV_FILE = EXPORT_CSV_FOLDER + File.separator + "position.csv"; + private static final String APPLICANT_CSV_HEADER = "Name,Phone,Email,Age,Address,Gender,Hire status,Tags"; + private static final String INTERVIEW_CSV_HEADER = "Date,Interview Status,Name,Phone,Email,Age,Address," + + "Gender,Hire status,Tags,Position,Description,Number of openings,Number of offers,Requirements"; + private static final String POSITION_CSV_HEADER = "Position,Description,Number of openings,Number of offers" + + "Requirements"; + private static final String MESSAGE_CLOSE_CSV = "Please close the opened CSV before export"; - private final AddressBook addressBook; + private final HireLah hireLah; private final UserPrefs userPrefs; - private final FilteredList filteredPersons; + private final FilteredList filteredApplicants; + private final FilteredList filteredInterviews; + private final FilteredList filteredPositions; /** - * Initializes a ModelManager with the given addressBook and userPrefs. + * Initializes a ModelManager with the given hireLah and userPrefs. */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { + public ModelManager(ReadOnlyHireLah addressBook, ReadOnlyUserPrefs userPrefs) { requireAllNonNull(addressBook, userPrefs); - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - this.addressBook = new AddressBook(addressBook); + this.hireLah = new HireLah(addressBook); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredApplicants = new FilteredList<>(this.hireLah.getApplicantList()); + filteredInterviews = new FilteredList<>(this.hireLah.getInterviewList()); + filteredPositions = new FilteredList<>(this.hireLah.getPositionList()); } public ModelManager() { - this(new AddressBook(), new UserPrefs()); + this(new HireLah(), new UserPrefs()); } //=========== UserPrefs ================================================================================== @@ -65,69 +89,255 @@ public void setGuiSettings(GuiSettings guiSettings) { } @Override - public Path getAddressBookFilePath() { - return userPrefs.getAddressBookFilePath(); + public Path getHireLahFilePath() { + return userPrefs.getHireLahFilePath(); } @Override - public void setAddressBookFilePath(Path addressBookFilePath) { + public void setHireLahFilePath(Path addressBookFilePath) { requireNonNull(addressBookFilePath); - userPrefs.setAddressBookFilePath(addressBookFilePath); + userPrefs.setHireLahFilePath(addressBookFilePath); + } + + //=========== HireLah ================================================================================ + + @Override + public void setHireLah(ReadOnlyHireLah hireLah) { + this.hireLah.resetData(hireLah); + } + + @Override + public ReadOnlyHireLah getHireLah() { + return hireLah; + } + + @Override + public boolean hasApplicant(Applicant applicant) { + requireNonNull(applicant); + return hireLah.hasApplicant(applicant); + } + + @Override + public Applicant getApplicantWithEmail(Email email) { + requireNonNull(email); + return hireLah.getApplicantWithEmail(email); } - //=========== AddressBook ================================================================================ + @Override + public Applicant getApplicantWithPhone(Phone phone) { + requireNonNull(phone); + return hireLah.getApplicantWithPhone(phone); + } @Override - public void setAddressBook(ReadOnlyAddressBook addressBook) { - this.addressBook.resetData(addressBook); + public void deleteApplicant(Applicant target) { + hireLah.removeApplicant(target); } @Override - public ReadOnlyAddressBook getAddressBook() { - return addressBook; + public void addApplicant(Applicant applicant) { + hireLah.addApplicant(applicant); + updateFilteredApplicantList(PREDICATE_SHOW_ALL_APPLICANTS); } @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return addressBook.hasPerson(person); + public void setApplicant(Applicant target, Applicant editedApplicant) { + requireAllNonNull(target, editedApplicant); + + hireLah.setApplicant(target, editedApplicant); } @Override - public void deletePerson(Person target) { - addressBook.removePerson(target); + public boolean hasInterview(Interview interview) { + requireNonNull(interview); + return hireLah.hasInterview(interview); } @Override - public void addPerson(Person person) { - addressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + public boolean hasConflictingInterview(Interview interview) { + requireAllNonNull(interview); + return hireLah.hasConflictingInterview(interview); } + @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); + public void deleteInterview(Interview target) { + hireLah.removeInterview(target); + } - addressBook.setPerson(target, editedPerson); + @Override + public void addInterview(Interview interview) { + hireLah.addInterview(interview); + updateFilteredInterviewList(PREDICATE_SHOW_ALL_INTERVIEWS); } - //=========== Filtered Person List Accessors ============================================================= + + @Override + public void setInterview(Interview target, Interview editedInterview) { + requireAllNonNull(target, editedInterview); + + hireLah.setInterview(target, editedInterview); + } + + @Override + public boolean hasPosition(Position position) { + requireNonNull(position); + return hireLah.hasPosition(position); + } + + @Override + public void addPosition(Position position) { + hireLah.addPosition(position); + updateFilteredPositionList(PREDICATE_SHOW_ALL_POSITIONS); + } + + @Override + public void deletePosition(Position target) { + hireLah.removePosition(target); + } + + @Override + public void setPosition(Position target, Position editedPosition) { + requireAllNonNull(target, editedPosition); + + hireLah.setPosition(target, editedPosition); + } + + @Override + public void updateApplicant(Applicant applicantToBeUpdated, Applicant newApplicant) { + requireAllNonNull(applicantToBeUpdated, newApplicant); + + hireLah.updateApplicant(applicantToBeUpdated, newApplicant); + } + + @Override + public void updatePosition(Position positionToBeUpdated, Position newPosition) { + requireAllNonNull(positionToBeUpdated, newPosition); + hireLah.updatePosition(positionToBeUpdated, newPosition); + } + + //=========== Filtered Applicant List Accessors ============================================================= /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of + * Returns an unmodifiable view of the list of {@code Applicant} backed by the internal list of * {@code versionedAddressBook} */ @Override - public ObservableList getFilteredPersonList() { - return filteredPersons; + public ObservableList getFilteredApplicantList() { + return filteredApplicants; } @Override - public void updateFilteredPersonList(Predicate predicate) { + public void updateFilteredApplicantList(Predicate predicate) { requireNonNull(predicate); - filteredPersons.setPredicate(predicate); + filteredApplicants.setPredicate(predicate); + } + // Need to test + @Override + public void updateSortApplicantList(Comparator comparator) { + requireNonNull(comparator); + hireLah.sortApplicant(comparator); + filteredApplicants.setPredicate(PREDICATE_SHOW_ALL_APPLICANTS); + } + + @Override + public void updateFilterAndSortApplicantList(Predicate predicate, Comparator comparator) { + requireAllNonNull(predicate, comparator); + hireLah.sortApplicant(comparator); + filteredApplicants.setPredicate(predicate); } + @Override + public void exportCsvApplicant() throws FileNotFoundException, ExportCsvOpenException { + exportCsv(APPLICANT_CSV_FILE, APPLICANT_CSV_HEADER, filteredApplicants.stream() + .map(Applicant::convertToCsv)); + } + + //=========== Filtered Interview List Accessors ============================================================= + + /** + * Returns an unmodifiable view of the list of {@code Interview} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList getFilteredInterviewList() { + return filteredInterviews; + } + + @Override + public void updateFilteredInterviewList(Predicate predicate) { + requireNonNull(predicate); + filteredInterviews.setPredicate(predicate); + } + + @Override + public void updateSortInterviewList(Comparator comparator) { + requireNonNull(comparator); + hireLah.sortInterview(comparator); + filteredInterviews.setPredicate(PREDICATE_SHOW_ALL_INTERVIEWS); + } + + @Override + public void updateFilterAndSortInterviewList(Predicate predicate, Comparator comparator) { + requireAllNonNull(predicate, comparator); + hireLah.sortInterview(comparator); + filteredInterviews.setPredicate(predicate); + } + + @Override + public ArrayList getApplicantsInterviews(Applicant applicant) { + return hireLah.getApplicantsInterviews(applicant); + } + + @Override + public ArrayList getPositionsInterviews(Position position) { + return hireLah.getPositionsInterview(position); + } + + @Override + public void exportCsvInterview() throws FileNotFoundException, ExportCsvOpenException { + exportCsv(INTERVIEW_CSV_FILE, INTERVIEW_CSV_HEADER, filteredInterviews.stream() + .map(Interview::convertToCsv)); + } + + @Override + public boolean isSameApplicantPosition(Applicant applicant, Position position) { + return hireLah.isSameApplicantPosition(applicant, position); + } + + //=========== Filtered Position List Accessors ============================================================= + @Override + public ObservableList getFilteredPositionList() { + return filteredPositions; + } + + @Override + public void updateFilteredPositionList(Predicate predicate) { + requireNonNull(predicate); + filteredPositions.setPredicate(predicate); + } + + @Override + public void updateSortPositionList(Comparator comparator) { + requireNonNull(comparator); + hireLah.sortPosition(comparator); + filteredPositions.setPredicate(PREDICATE_SHOW_ALL_POSITIONS); + } + + @Override + public void updateFilterAndSortPositionList(Predicate predicate, Comparator comparator) { + requireAllNonNull(predicate, comparator); + hireLah.sortPosition(comparator); + filteredPositions.setPredicate(predicate); + } + + @Override + public void exportCsvPosition() throws FileNotFoundException, ExportCsvOpenException { + exportCsv(POSITION_CSV_FILE, POSITION_CSV_HEADER, filteredPositions.stream() + .map(Position::convertToCsv)); + } + + //=========== Utility methods ============================================================= @Override public boolean equals(Object obj) { // short circuit if same object @@ -142,9 +352,27 @@ public boolean equals(Object obj) { // state check ModelManager other = (ModelManager) obj; - return addressBook.equals(other.addressBook) + return hireLah.equals(other.hireLah) && userPrefs.equals(other.userPrefs) - && filteredPersons.equals(other.filteredPersons); + && filteredApplicants.equals(other.filteredApplicants) + && filteredPositions.equals(other.filteredPositions) + && filteredInterviews.equals(other.filteredInterviews); } + private void exportCsv(String csvFile, String csvHeader, Stream stringStream) + throws ExportCsvOpenException { + File csvOutputFile = new File(csvFile); + File directory = new File(EXPORT_CSV_FOLDER); + if (!directory.exists()) { + directory.mkdirs(); + } + try (PrintWriter pw = new PrintWriter(csvOutputFile)) { + pw.println(csvHeader); + stringStream + .forEach(pw::println); + assert (csvOutputFile.exists()); + } catch (FileNotFoundException exception) { + throw new ExportCsvOpenException(MESSAGE_CLOSE_CSV); + } + } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java deleted file mode 100644 index 6ddc2cd9a29..00000000000 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ /dev/null @@ -1,17 +0,0 @@ -package seedu.address.model; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; - -/** - * Unmodifiable view of an address book - */ -public interface ReadOnlyAddressBook { - - /** - * Returns an unmodifiable view of the persons list. - * This list will not contain any duplicate persons. - */ - ObservableList getPersonList(); - -} diff --git a/src/main/java/seedu/address/model/ReadOnlyHireLah.java b/src/main/java/seedu/address/model/ReadOnlyHireLah.java new file mode 100644 index 00000000000..d3ece27370f --- /dev/null +++ b/src/main/java/seedu/address/model/ReadOnlyHireLah.java @@ -0,0 +1,31 @@ +package seedu.address.model; + +import javafx.collections.ObservableList; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; + +/** + * Unmodifiable view of an HireLah + */ +public interface ReadOnlyHireLah { + + /** + * Returns an unmodifiable view of the persons list. + * This list will not contain any duplicate persons. + */ + ObservableList getApplicantList(); + + /** + * Returns an unmodifiable view of the interview list. + * This list will not contain any duplicate interviews. + */ + ObservableList getInterviewList(); + + + /** + * Returns an unmodifiable view of the position list. + * This list will not contain any duplicate positions. + */ + ObservableList getPositionList(); +} diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java index befd58a4c73..6e92c8105af 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java @@ -11,6 +11,6 @@ public interface ReadOnlyUserPrefs { GuiSettings getGuiSettings(); - Path getAddressBookFilePath(); + Path getHireLahFilePath(); } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 25a5fd6eab9..df017bbf637 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; +import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Objects; @@ -14,7 +15,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path hireLahFilePath = Paths.get("data" + File.separator + "HireLah.json"); /** * Creates a {@code UserPrefs} with default values. @@ -35,7 +36,7 @@ public UserPrefs(ReadOnlyUserPrefs userPrefs) { public void resetData(ReadOnlyUserPrefs newUserPrefs) { requireNonNull(newUserPrefs); setGuiSettings(newUserPrefs.getGuiSettings()); - setAddressBookFilePath(newUserPrefs.getAddressBookFilePath()); + setHireLahFilePath(newUserPrefs.getHireLahFilePath()); } public GuiSettings getGuiSettings() { @@ -47,13 +48,13 @@ public void setGuiSettings(GuiSettings guiSettings) { this.guiSettings = guiSettings; } - public Path getAddressBookFilePath() { - return addressBookFilePath; + public Path getHireLahFilePath() { + return hireLahFilePath; } - public void setAddressBookFilePath(Path addressBookFilePath) { - requireNonNull(addressBookFilePath); - this.addressBookFilePath = addressBookFilePath; + public void setHireLahFilePath(Path hireLahFilePath) { + requireNonNull(hireLahFilePath); + this.hireLahFilePath = hireLahFilePath; } @Override @@ -68,19 +69,19 @@ public boolean equals(Object other) { UserPrefs o = (UserPrefs) other; return guiSettings.equals(o.guiSettings) - && addressBookFilePath.equals(o.addressBookFilePath); + && hireLahFilePath.equals(o.hireLahFilePath); } @Override public int hashCode() { - return Objects.hash(guiSettings, addressBookFilePath); + return Objects.hash(guiSettings, hireLahFilePath); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Gui Settings : " + guiSettings); - sb.append("\nLocal data file location : " + addressBookFilePath); + sb.append("\nLocal data file location : " + hireLahFilePath); return sb.toString(); } diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/applicant/Address.java similarity index 71% rename from src/main/java/seedu/address/model/person/Address.java rename to src/main/java/seedu/address/model/applicant/Address.java index 60472ca22a0..8903bb0e5ab 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/applicant/Address.java @@ -1,21 +1,23 @@ -package seedu.address.model.person; +package seedu.address.model.applicant; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's address in the address book. + * Represents a Applicant's address in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} */ public class Address { - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; - + public static final String MESSAGE_CONSTRAINTS = + "Address can contain any characters and spaces, and it should not be blank.\n" + + "Address should contain at least one alphanumeric character (e.g. \"1\" or \"a\")\n" + + "Length of address is restricted to a maximum of 100 characters."; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[^\\s].*"; + public static final String VALIDATION_REGEX = "(?=.*[\\p{Alnum}].*)[^\\s].{0,99}"; public final String value; diff --git a/src/main/java/seedu/address/model/applicant/Age.java b/src/main/java/seedu/address/model/applicant/Age.java new file mode 100644 index 00000000000..86393f0a403 --- /dev/null +++ b/src/main/java/seedu/address/model/applicant/Age.java @@ -0,0 +1,48 @@ +package seedu.address.model.applicant; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +public class Age { + public static final String MESSAGE_CONSTRAINTS = + "Age should only contain numbers, it should be 2 digits long and should not start with 0"; + public static final String VALIDATION_REGEX = "[1-9][0-9]"; + public final String value; + + /** + * Constructs an {@code age}. + * + * @param age A valid age . + */ + public Age(String age) { + requireNonNull(age); + checkArgument(isValidAge(age), MESSAGE_CONSTRAINTS); + this.value = age; + } + + /** + * Returns true if a given string is a valid age. + */ + public static boolean isValidAge(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Age // instanceof handles nulls + && value.equals(((Age) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + +} diff --git a/src/main/java/seedu/address/model/applicant/Applicant.java b/src/main/java/seedu/address/model/applicant/Applicant.java new file mode 100644 index 00000000000..2971dc1160b --- /dev/null +++ b/src/main/java/seedu/address/model/applicant/Applicant.java @@ -0,0 +1,214 @@ +package seedu.address.model.applicant; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import seedu.address.model.position.Position; +import seedu.address.model.tag.Tag; + +/** + * Represents a Applicant in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Applicant { + + // Identity fields + private final Name name; + private final Phone phone; + private final Email email; + + // Data fields + private final Age age; + private final Address address; + private final Gender gender; + private final HiredStatus hiredStatus; + private final Set tags = new HashSet<>(); + + /** + * Every field must be present and not null. + * For initializing an Applicant object with compulsory fields and default values + */ + public Applicant(Name name, Phone phone, Email email, Age age, Address address, Gender gender, Set tags) { + requireAllNonNull(name, phone, email, age, address, gender, tags); + this.name = name; + this.phone = phone; + this.email = email; + this.age = age; + this.address = address; + this.gender = gender; + this.hiredStatus = new HiredStatus(); + this.tags.addAll(tags); + } + + /** + * Overloaded constructor used to edit applicant with changed values + */ + public Applicant(Name name, Phone phone, Email email, Age age, Address address, Gender gender, + HiredStatus hiredStatus, Set tags) { + requireAllNonNull(name, phone, email, age, address, gender, tags); + this.name = name; + this.phone = phone; + this.email = email; + this.age = age; + this.address = address; + this.gender = gender; + this.hiredStatus = hiredStatus; + this.tags.addAll(tags); + } + /** + * Changes the hiredStatus of an applicant to the name of the Position provided + */ + public Applicant setStatus(Applicant applicant, Position position) { + return new Applicant(applicant.getName(), + applicant.getPhone(), + applicant.getEmail(), + applicant.getAge(), + applicant.getAddress(), + applicant.getGender(), + new HiredStatus(position.getPositionName().toString()), + applicant.getTags()); + } + + public Name getName() { + return name; + } + + public Phone getPhone() { + return phone; + } + + public Email getEmail() { + return email; + } + + public Age getAge() { + return age; + } + + public Address getAddress() { + return address; + } + + public Gender getGender() { + return gender; + } + + public HiredStatus getStatus() { + return hiredStatus; + } + + public boolean isHired() { + return hiredStatus.isHired(); + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + /** + * Returns true if both persons have the same name. + * This defines a weaker notion of equality between two persons. + */ + public boolean isSameApplicant(Applicant otherApplicant) { + if (otherApplicant == this) { + return true; + } + + return otherApplicant != null + && otherApplicant.getName().equals(getName()); + } + + /** + * Returns true if both persons have the same identity and data fields. + * This defines a stronger notion of equality between two persons. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Applicant)) { + return false; + } + + Applicant otherApplicant = (Applicant) other; + return otherApplicant.getName().equals(getName()) + && otherApplicant.getPhone().equals(getPhone()) + && otherApplicant.getEmail().equals(getEmail()) + && otherApplicant.getAge().equals(getAge()) + && otherApplicant.getAddress().equals(getAddress()) + && otherApplicant.getGender().equals(getGender()) + && otherApplicant.getTags().equals(getTags()) + && otherApplicant.getStatus().equals(getStatus()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(name, phone, email, age, address, gender, tags); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getName()) + .append("; Phone: ") + .append(getPhone()) + .append("; Email: ") + .append(getEmail()) + .append("; Age: ") + .append(getAge()) + .append("; Address: ") + .append(getAddress()) + .append("; Gender: ") + .append(getGender()) + .append("; HiredStatus: ") + .append(getStatus()); + + Set tags = getTags(); + if (!tags.isEmpty()) { + builder.append("; Tags: "); + tags.forEach(builder::append); + } + return builder.toString(); + } + + /** + * Creates csv output for applicant + */ + public String convertToCsv() { + StringBuilder tagString = new StringBuilder(); + for (Tag tag : tags) { + tagString.append(tag.tagName); + tagString.append(" | "); + } + String trimmedTagString = tagString.substring(0, tagString.length() - 3); + return name.fullName + "," + phone.value + "," + email.value + "," + age.value + "," + + escapeSpecialCharacters(address.value) + "," + + gender.value + "," + hiredStatus.toString() + "," + trimmedTagString; + } + + /** + * Eliminates special characters from csv string + */ + //@@author Javi + //Reused from https://stackoverflow.com/questions/61680044/ + // write-elements-of-a-map-to-a-csv-correctly-in-a-simplified-way-in-java-8 + private String escapeSpecialCharacters(String data) { + String escapedData = data.replaceAll("\\R", " "); + if (data.contains(",") || data.contains("\"") || data.contains("'")) { + data = data.replace("\"", "\"\""); + escapedData = "\"" + data + "\""; + } + return escapedData; + } +} diff --git a/src/main/java/seedu/address/model/applicant/ApplicantGenderPredicate.java b/src/main/java/seedu/address/model/applicant/ApplicantGenderPredicate.java new file mode 100644 index 00000000000..2b32e393fee --- /dev/null +++ b/src/main/java/seedu/address/model/applicant/ApplicantGenderPredicate.java @@ -0,0 +1,27 @@ +package seedu.address.model.applicant; + +import java.util.function.Predicate; + +/** + * Tests that an {@code Applicant}'s {@code Gender} matches the gender given. + */ +public class ApplicantGenderPredicate implements Predicate { + private final String gender; + + public ApplicantGenderPredicate(String gender) { + this.gender = gender; + } + + @Override + public boolean test(Applicant applicant) { + assert gender.equals("M") || gender.equals("F"); + return applicant.getGender().value.equals(gender); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ApplicantGenderPredicate // instanceof handles nulls + && gender.equals(((ApplicantGenderPredicate) other).gender)); // state check + } +} diff --git a/src/main/java/seedu/address/model/applicant/ApplicantNameComparator.java b/src/main/java/seedu/address/model/applicant/ApplicantNameComparator.java new file mode 100644 index 00000000000..f8fcbaaf419 --- /dev/null +++ b/src/main/java/seedu/address/model/applicant/ApplicantNameComparator.java @@ -0,0 +1,32 @@ +package seedu.address.model.applicant; + +import java.util.Comparator; + +public class ApplicantNameComparator implements Comparator { + private final String sortArgument; + + public ApplicantNameComparator(String sortArgument) { + this.sortArgument = sortArgument; + } + + @Override + public int compare(Applicant o1, Applicant o2) { + if (sortArgument.equals("asc")) { + return o1.getName().fullName.compareTo(o2.getName().fullName); + } else { + return o2.getName().fullName.compareTo(o1.getName().fullName); + } + } + + @Override + public boolean equals(Object obj) { + return obj == this + || (obj instanceof ApplicantNameComparator + && sortArgument.equals(((ApplicantNameComparator) obj).sortArgument)); + } + + @Override + public String toString() { + return this.sortArgument; + } +} diff --git a/src/main/java/seedu/address/model/applicant/ApplicantNamePredicate.java b/src/main/java/seedu/address/model/applicant/ApplicantNamePredicate.java new file mode 100644 index 00000000000..1979bc37b0e --- /dev/null +++ b/src/main/java/seedu/address/model/applicant/ApplicantNamePredicate.java @@ -0,0 +1,30 @@ +package seedu.address.model.applicant; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Applicant}'s {@code Name} matches any of the keywords given. + */ +public class ApplicantNamePredicate implements Predicate { + private final List keywords; + + public ApplicantNamePredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Applicant applicant) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(applicant.getName().fullName, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ApplicantNamePredicate // instanceof handles nulls + && keywords.equals(((ApplicantNamePredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/applicant/ApplicantStatusPredicate.java b/src/main/java/seedu/address/model/applicant/ApplicantStatusPredicate.java new file mode 100644 index 00000000000..fae14f2414d --- /dev/null +++ b/src/main/java/seedu/address/model/applicant/ApplicantStatusPredicate.java @@ -0,0 +1,29 @@ +package seedu.address.model.applicant; + +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that an {@code Applicant}'s {@code HiredStatus} matches the status given. + */ +public class ApplicantStatusPredicate implements Predicate { + private final String status; + + public ApplicantStatusPredicate(String status) { + this.status = status; + } + + @Override + public boolean test(Applicant applicant) { + assert status.equals("available") || status.equals("hired"); + return StringUtil.containsWordIgnoreCase(applicant.getStatus().toString(), status); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ApplicantStatusPredicate // instanceof handles nulls + && status.equals(((ApplicantStatusPredicate) other).status)); // state check + } +} diff --git a/src/main/java/seedu/address/model/applicant/ApplicantTagPredicate.java b/src/main/java/seedu/address/model/applicant/ApplicantTagPredicate.java new file mode 100644 index 00000000000..ee3f13ac349 --- /dev/null +++ b/src/main/java/seedu/address/model/applicant/ApplicantTagPredicate.java @@ -0,0 +1,35 @@ +package seedu.address.model.applicant; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.model.tag.Tag; + +/** + * Tests that an {@code Applicant} has a {@code Tag} that matches any of the keywords given. + */ +public class ApplicantTagPredicate implements Predicate { + private final List keywords; + + public ApplicantTagPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Applicant applicant) { + for (Tag tag : applicant.getTags()) { + if (keywords.stream().anyMatch(keyword -> StringUtil.containsWordIgnoreCase(tag.tagName, keyword))) { + return true; + } + } + return false; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ApplicantTagPredicate // instanceof handles nulls + && keywords.equals(((ApplicantTagPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/applicant/Email.java similarity index 73% rename from src/main/java/seedu/address/model/person/Email.java rename to src/main/java/seedu/address/model/applicant/Email.java index f866e7133de..c2466c0a930 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/applicant/Email.java @@ -1,26 +1,29 @@ -package seedu.address.model.person; +package seedu.address.model.applicant; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's email in the address book. + * Represents a Applicant's email in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} */ public class Email { + public static final int MAX_TEXT_LENGTH = 100; private static final String SPECIAL_CHARACTERS = "+_.-"; public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " + "and adhere to the following constraints:\n" - + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " - + "the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " + + "1. The local-part should only contain alphanumeric characters and these special characters, excluding\n" + + " the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " + "characters.\n" + "2. This is followed by a '@' and then a domain name. The domain name is made up of domain labels " + "separated by periods.\n" - + "The domain name must:\n" - + " - end with a domain label at least 2 characters long\n" - + " - have each domain label start and end with alphanumeric characters\n" - + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any."; + + " The domain name must:\n" + + " - end with a domain label at least 2 characters long\n" + + " - have each domain label start and end with alphanumeric characters\n" + + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any." + + "\n" + + "3. Length of email address is restricted to a maximum of 100 characters"; // alphanumeric and special characters private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]+"; // alphanumeric characters except underscore private static final String LOCAL_PART_REGEX = "^" + ALPHANUMERIC_NO_UNDERSCORE + "([" + SPECIAL_CHARACTERS + "]" @@ -48,7 +51,7 @@ public Email(String email) { * Returns if a given string is a valid email. */ public static boolean isValidEmail(String test) { - return test.matches(VALIDATION_REGEX); + return test.matches(VALIDATION_REGEX) && test.length() <= MAX_TEXT_LENGTH; } @Override diff --git a/src/main/java/seedu/address/model/applicant/Gender.java b/src/main/java/seedu/address/model/applicant/Gender.java new file mode 100644 index 00000000000..c6f2e3981d3 --- /dev/null +++ b/src/main/java/seedu/address/model/applicant/Gender.java @@ -0,0 +1,53 @@ +package seedu.address.model.applicant; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Applicant's gender in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidGender(String)} + */ +public class Gender { + + public static final String MESSAGE_CONSTRAINTS = + "Gender should be exactly one character and only M or F (case sensitive)"; + + public static final String VALIDATION_REGEX = "M|F"; + public final String value; + + /** + * Constructs a {@code Gender}. + * + * @param gender A valid gender. + */ + public Gender(String gender) { + requireNonNull(gender); + checkArgument(isValidGender(gender), MESSAGE_CONSTRAINTS); + value = gender; + } + + /** + * Returns true if a given string is a valid gender. + */ + public static boolean isValidGender(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Gender // instanceof handles nulls + && value.equals(((Gender) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/applicant/HiredStatus.java b/src/main/java/seedu/address/model/applicant/HiredStatus.java new file mode 100644 index 00000000000..345eb4bedda --- /dev/null +++ b/src/main/java/seedu/address/model/applicant/HiredStatus.java @@ -0,0 +1,55 @@ +package seedu.address.model.applicant; + +/** + * Represents an Applicant's status in the address book. + * default value is "Available" and is replaced with a position name when Applicant accepts an offer + */ +public class HiredStatus { + private final String value; + + /** + * Constructs a {@code HiredStatus}. + * Initialised as available as every Applicant added to HireLah is applying for a position. + */ + public HiredStatus() { + value = "Available"; + } + + public HiredStatus(String positionName) { + value = positionName; + } + + public boolean isHired() { + return !value.equals("Available"); + } + + @Override + public String toString() { + if (value.equals("Available")) { + return value; + } else { + return "Hired as " + value; + } + } + + public String getValue() { + return this.value; + } + + /** + * Returns true if both Hired Status have the same value. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof HiredStatus)) { + return false; + } + + HiredStatus otherStatus = (HiredStatus) other; + return otherStatus.value.equals(this.value); + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/applicant/Name.java similarity index 76% rename from src/main/java/seedu/address/model/person/Name.java rename to src/main/java/seedu/address/model/applicant/Name.java index 79244d71cf7..16ca1e48a75 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/applicant/Name.java @@ -1,22 +1,24 @@ -package seedu.address.model.person; +package seedu.address.model.applicant; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's name in the address book. + * Represents a Applicant's name in the address book. * 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 only contain alphanumeric characters and spaces, and it should not be blank.\n" + + "Name should contain at least one alphabetic character (e.g. \"a\")\n" + + "Length of name is restricted to a maximum of 100 characters."; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "(?=.*[\\p{Alpha}].*)[\\p{Alnum}][\\p{Alnum} ]{0,99}"; public final String fullName; diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/applicant/Phone.java similarity index 77% rename from src/main/java/seedu/address/model/person/Phone.java rename to src/main/java/seedu/address/model/applicant/Phone.java index 872c76b382f..795d3215734 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/applicant/Phone.java @@ -1,18 +1,18 @@ -package seedu.address.model.person; +package seedu.address.model.applicant; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's phone number in the address book. + * Represents a Applicant's phone number in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} */ public class Phone { - + public static final int MAX_PHONE_LENGTH = 15; public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String VALIDATION_REGEX = "\\d{3,}"; + "Phone numbers should only contain numbers, and it should be between 3 to 15 digits long."; + public static final String VALIDATION_REGEX = "\\d{3,15}"; public final String value; /** @@ -30,7 +30,7 @@ public Phone(String phone) { * Returns true if a given string is a valid phone number. */ public static boolean isValidPhone(String test) { - return test.matches(VALIDATION_REGEX); + return test.matches(VALIDATION_REGEX) && test.length() <= MAX_PHONE_LENGTH; } @Override diff --git a/src/main/java/seedu/address/model/applicant/UniqueApplicantList.java b/src/main/java/seedu/address/model/applicant/UniqueApplicantList.java new file mode 100644 index 00000000000..d21c7f534b7 --- /dev/null +++ b/src/main/java/seedu/address/model/applicant/UniqueApplicantList.java @@ -0,0 +1,180 @@ +package seedu.address.model.applicant; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.applicant.exceptions.ApplicantNotFoundException; +import seedu.address.model.applicant.exceptions.DuplicateApplicantException; + +/** + * A list of persons that enforces uniqueness between its elements and does not allow nulls. + * A applicant is considered unique by comparing using {@code Applicant#isSameApplicant(Applicant)}. As such, adding and + * updating of persons uses Applicant#isSameApplicant(Applicant) for equality so as to ensure that the applicant being + * added or updated is unique in terms of identity in the UniqueApplicantList. However, the removal of a applicant uses + * Applicant#equals(Object) so as to ensure that the applicant with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + * @see Applicant#isSameApplicant(Applicant) + */ +public class UniqueApplicantList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent applicant as the given argument. + */ + public boolean contains(Applicant toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameApplicant); + } + + /** + * Adds a applicant to the list. + * The applicant must not already exist in the list. + */ + public void add(Applicant toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateApplicantException(); + } + internalList.add(toAdd); + } + + /** + * Sorts a list of applicant + */ + public void sort(Comparator comparator) { + requireNonNull(comparator); + internalList.sort(comparator); + } + + /** + * Replaces the applicant {@code target} in the list with {@code editedApplicant}. + * {@code target} must exist in the list. + * The applicant identity of {@code editedApplicant} must not be the same as another existing applicant in the list. + */ + public void setApplicant(Applicant target, Applicant editedApplicant) { + requireAllNonNull(target, editedApplicant); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new ApplicantNotFoundException(); + } + + if (!target.isSameApplicant(editedApplicant) && contains(editedApplicant)) { + throw new DuplicateApplicantException(); + } + + internalList.set(index, editedApplicant); + } + + /** + * Removes the equivalent applicant from the list. + * The applicant must exist in the list. + */ + public void remove(Applicant toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new ApplicantNotFoundException(); + } + } + + public void setApplicants(UniqueApplicantList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code applicants}. + * {@code applicants} must not contain duplicate applicants. + */ + public void setApplicants(List applicants) { + requireAllNonNull(applicants); + if (!applicantsAreUnique(applicants)) { + throw new DuplicateApplicantException(); + } + + internalList.setAll(applicants); + } + + /** + * Returns the {@code Applicant} with the {@code email} provided if exists; or null if no such applicant. + */ + public Applicant getApplicantWithEmail(Email email) { + requireNonNull(email); + + for (Applicant a : internalList) { + if (email.equals(a.getEmail())) { + return a; + } + } + return null; + } + + /** + * Returns the {@code Applicant} with the {@code phone} provided if exists; or null if no such applicant. + */ + public Applicant getApplicantWithPhone(Phone phone) { + requireNonNull(phone); + + for (Applicant a : internalList) { + if (phone.equals(a.getPhone())) { + return a; + } + } + return null; + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueApplicantList // instanceof handles nulls + && internalList.equals(((UniqueApplicantList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code applicants} contains only unique applicants. + */ + private boolean applicantsAreUnique(List applicants) { + for (int i = 0; i < applicants.size() - 1; i++) { + for (int j = i + 1; j < applicants.size(); j++) { + if (applicants.get(i).isSameApplicant(applicants.get(j))) { + return false; + } + } + } + return true; + } + // May change isSameApplicant to equals if required + public Applicant getApplicant(Applicant interviewApplicant) { + return internalList.stream().filter(a -> a.isSameApplicant(interviewApplicant)) + .collect(Collectors.toList()).get(0); + } +} diff --git a/src/main/java/seedu/address/model/applicant/exceptions/ApplicantNotFoundException.java b/src/main/java/seedu/address/model/applicant/exceptions/ApplicantNotFoundException.java new file mode 100644 index 00000000000..9be4a5a0e99 --- /dev/null +++ b/src/main/java/seedu/address/model/applicant/exceptions/ApplicantNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.applicant.exceptions; + +/** + * Signals that the operation is unable to find the specified applicant. + */ +public class ApplicantNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/applicant/exceptions/DuplicateApplicantException.java similarity index 56% rename from src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java rename to src/main/java/seedu/address/model/applicant/exceptions/DuplicateApplicantException.java index d7290f59442..33299582aaa 100644 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ b/src/main/java/seedu/address/model/applicant/exceptions/DuplicateApplicantException.java @@ -1,11 +1,11 @@ -package seedu.address.model.person.exceptions; +package seedu.address.model.applicant.exceptions; /** * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same * identity). */ -public class DuplicatePersonException extends RuntimeException { - public DuplicatePersonException() { +public class DuplicateApplicantException extends RuntimeException { + public DuplicateApplicantException() { super("Operation would result in duplicate persons"); } } diff --git a/src/main/java/seedu/address/model/interview/Interview.java b/src/main/java/seedu/address/model/interview/Interview.java new file mode 100644 index 00000000000..d6642662a66 --- /dev/null +++ b/src/main/java/seedu/address/model/interview/Interview.java @@ -0,0 +1,213 @@ +package seedu.address.model.interview; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import seedu.address.model.applicant.Applicant; +import seedu.address.model.position.Position; + + +/** + * Represents an Interview in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Interview { + + //Data fields + private Applicant applicant; + private final LocalDateTime date; + private Position position; + private final Status status; + + /** + * Every field must be present and not null. + */ + public Interview(Applicant applicant, LocalDateTime date, Position position) { + requireAllNonNull(applicant, date, position); + this.applicant = applicant; + this.date = date; + this.position = position; + this.status = new Status(); + } + + /** + * Create Interview object when loading from database + * Every field must be present and not null. + */ + public Interview(Applicant applicant, LocalDateTime date, Position position, Status status) { + requireAllNonNull(applicant, date, status, position); + this.applicant = applicant; + this.date = date; + this.position = position; + this.status = status; + } + + public Applicant getApplicant() { + return applicant; + } + + public LocalDateTime getDate() { + return date; + } + + public Position getPosition() { + return position; + } + + public Status getStatus() { + return status; + } + + public void setApplicant(Applicant applicant) { + this.applicant = applicant; + } + + public void setPosition(Position position) { + this.position = position; + } + + /** + * Checks if the interview is for the specified applicant. + */ + public boolean isInterviewForApplicant(Applicant a) { + return applicant.isSameApplicant(a); + } + + /** + * Checks if the interview is for the specified position. + */ + public boolean isInterviewForPosition(Position p) { + return position.isSamePosition(p); + } + + /** + * Checks if the given interview will conflict with the current interview. + */ + public boolean isConflictingInterview(Interview i) { + boolean isSameApplicant = i.isInterviewForApplicant(this.applicant); + + // Interview has to be at least 1 hour before or after the current interview time for it not to clash + return isSameApplicant && !(i.date.isBefore(this.date.minusMinutes(59)) + || i.date.isAfter(this.date.plusMinutes(59))); + } + + /** + * Checks if the given interview can be passed based on the number of offers given for its position. + */ + public boolean isPassableInterview() { + return this.position.canExtendOffer(); + } + + /** + * Checks if the given interview has Pending status + */ + public boolean isPendingStatus() { + return status.isPendingStatus(); + } + + /** + * Checks if the given interview can be passed based on the number of offers given for its position. + */ + public boolean isAcceptableInterview() { + return status.isPassedStatus() && this.position.canAcceptOffer(); + } + + /** + * Checks if the current interview can be failed. + */ + public boolean isFailableInterview() { + return isPendingStatus(); + } + + /** + * Checks if the given interview can be rejected based on the number of offers. + */ + public boolean isRejectableInterview() { + return status.isPassedStatus() && this.position.canRejectOffer(); + } + + + + /** + * Marks an interview as passed and increments the position offering. + */ + public void markAsPassed() { + this.status.markAsPassed(); + } + + /** + * Marks an interview as failed. + */ + public void markAsFailed() { + this.status.markAsFailed(); + } + + /** + * Marks an interview as accepted. + * The interview must already have been passed to be accepted. + */ + public void markAsAccepted() { + this.status.markAsAccepted(); + } + + /** + * Marks an interview as rejected. + * The interview must already have been passed to be rejected. + */ + public void markAsRejected() { + this.status.markAsRejected(); + } + + /** + * Checks whether an interview is passed. + */ + public boolean isPassedStatus() { + return this.status.isPassedStatus(); + } + + /** + * Returns true if both interviews have the same data fields. + * This defines a stronger notion of equality between two interviews. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Interview)) { + return false; + } + + Interview otherInterview = (Interview) other; + return otherInterview.getApplicant().equals(getApplicant()) + && otherInterview.getDate().equals(getDate()) + && otherInterview.getStatus().equals(getStatus()) + && otherInterview.getPosition().equals(getPosition()); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(applicant.getName()) + .append("; Date: ") + .append(getDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))) + .append("; Position: ") + .append(position.getPositionName()) + .append("; Status: ") + .append(getStatus()); + return builder.toString(); + } + + /** + * Creates csv output for interview + */ + public String convertToCsv() { + String applicantCsv = this.applicant.convertToCsv(); + String positionCsv = this.position.convertToCsv(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); + return this.date.format(formatter) + "," + this.status + "," + applicantCsv + "," + positionCsv; + } +} diff --git a/src/main/java/seedu/address/model/interview/InterviewApplicantPredicate.java b/src/main/java/seedu/address/model/interview/InterviewApplicantPredicate.java new file mode 100644 index 00000000000..734936d1078 --- /dev/null +++ b/src/main/java/seedu/address/model/interview/InterviewApplicantPredicate.java @@ -0,0 +1,31 @@ +package seedu.address.model.interview; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Interview}'s {@code Applicant}'s {@code name} matches any of the keywords given. + */ +public class InterviewApplicantPredicate implements Predicate { + private final List keywords; + + public InterviewApplicantPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Interview interview) { + return keywords.stream() + .anyMatch(keyword -> + StringUtil.containsWordIgnoreCase(interview.getApplicant().getName().fullName, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof InterviewApplicantPredicate // instanceof handles nulls + && keywords.equals(((InterviewApplicantPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/interview/InterviewDateComparator.java b/src/main/java/seedu/address/model/interview/InterviewDateComparator.java new file mode 100644 index 00000000000..db9393580e3 --- /dev/null +++ b/src/main/java/seedu/address/model/interview/InterviewDateComparator.java @@ -0,0 +1,20 @@ +package seedu.address.model.interview; + +import java.util.Comparator; + +public class InterviewDateComparator implements Comparator { + private final String sortArgument; + + public InterviewDateComparator(String sortArgument) { + this.sortArgument = sortArgument; + } + + @Override + public int compare(Interview o1, Interview o2) { + if (sortArgument.equals("asc")) { + return o1.getDate().compareTo(o2.getDate()); + } else { + return o2.getDate().compareTo(o1.getDate()); + } + } +} diff --git a/src/main/java/seedu/address/model/interview/InterviewDatePredicate.java b/src/main/java/seedu/address/model/interview/InterviewDatePredicate.java new file mode 100644 index 00000000000..fbb22c9b044 --- /dev/null +++ b/src/main/java/seedu/address/model/interview/InterviewDatePredicate.java @@ -0,0 +1,27 @@ +package seedu.address.model.interview; + +import java.time.LocalDate; +import java.util.function.Predicate; + +/** + * Tests that a {@code Interview}'s {@code date} matches the given date. + */ +public class InterviewDatePredicate implements Predicate { + private final LocalDate date; + + public InterviewDatePredicate(LocalDate date) { + this.date = date; + } + + @Override + public boolean test(Interview interview) { + return date.isEqual(interview.getDate().toLocalDate()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof InterviewDatePredicate // instanceof handles nulls + && date.equals(((InterviewDatePredicate) other).date)); // state check + } +} diff --git a/src/main/java/seedu/address/model/interview/InterviewPositionPredicate.java b/src/main/java/seedu/address/model/interview/InterviewPositionPredicate.java new file mode 100644 index 00000000000..83e8f3c4c43 --- /dev/null +++ b/src/main/java/seedu/address/model/interview/InterviewPositionPredicate.java @@ -0,0 +1,32 @@ +package seedu.address.model.interview; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Interview}'s {@code Position}'s {@code name} matches any of the keywords given. + */ +public class InterviewPositionPredicate implements Predicate { + private final List keywords; + + public InterviewPositionPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Interview interview) { + return keywords.stream() + .anyMatch(keyword -> + StringUtil.containsWordIgnoreCase( + interview.getPosition().getPositionName().positionName, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof InterviewPositionPredicate // instanceof handles nulls + && keywords.equals(((InterviewPositionPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/interview/InterviewStatusPredicate.java b/src/main/java/seedu/address/model/interview/InterviewStatusPredicate.java new file mode 100644 index 00000000000..f9620ad1f25 --- /dev/null +++ b/src/main/java/seedu/address/model/interview/InterviewStatusPredicate.java @@ -0,0 +1,30 @@ +package seedu.address.model.interview; + +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that an {@code Interview}'s {@code Status} matches the status given. + */ +public class InterviewStatusPredicate implements Predicate { + private final String status; + + public InterviewStatusPredicate(String status) { + this.status = status; + } + + @Override + public boolean test(Interview interview) { + assert status.equals("pending") || status.equals("passed") || status.equals("failed") + || status.equals("accepted") || status.equals("rejected"); + return StringUtil.containsWordIgnoreCase(interview.getStatus().toString(), status); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof InterviewStatusPredicate // instanceof handles nulls + && status.equals(((InterviewStatusPredicate) other).status)); // state check + } +} diff --git a/src/main/java/seedu/address/model/interview/Status.java b/src/main/java/seedu/address/model/interview/Status.java new file mode 100644 index 00000000000..a91f586d592 --- /dev/null +++ b/src/main/java/seedu/address/model/interview/Status.java @@ -0,0 +1,83 @@ +package seedu.address.model.interview; + +/** + * Represents the status of interview in HireLah. + * Guarantees: is always initialized as "Pending" {@link #Status()} + */ +public class Status { + + private String value; + + /** + * Constructs a Status with default value of "Pending". + */ + public Status() { + this("Pending"); + } + + public Status(String value) { + this.value = value; + } + + public void markAsPassed() { + value = "Passed - Waiting for Applicant"; + } + + public void markAsFailed() { + value = "Failed"; + } + + /** + * Marks an interview as accepted only if status is passed. + */ + public void markAsAccepted() { + // Defensive programming to prevent acceptance before passing interview + if (!isPassedStatus()) { + throw new RuntimeException("The Interview should be passed before its can be accepted by candidate"); + } + value = "Accepted"; + } + + /** + * Marks an interview as rejected only if status is passed. + */ + public void markAsRejected() { + // Defensive programming to prevent rejecting before passing interview + if (!isPassedStatus()) { + throw new RuntimeException("The Interview should be passed before its can be accepted by candidate"); + } + value = "Rejected"; + } + + /** + * Checks whether the current status is Pending. + */ + public boolean isPendingStatus() { + return value.equals("Pending"); + } + + /** + * Checks whether the current status is Passed. + */ + public boolean isPassedStatus() { + return value.equals("Passed - Waiting for Applicant"); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Status // instanceof handles nulls + && value.equals(((Status) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/interview/UniqueInterviewList.java b/src/main/java/seedu/address/model/interview/UniqueInterviewList.java new file mode 100644 index 00000000000..f086b3f5480 --- /dev/null +++ b/src/main/java/seedu/address/model/interview/UniqueInterviewList.java @@ -0,0 +1,220 @@ +package seedu.address.model.interview; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.interview.exceptions.DuplicateInterviewException; +import seedu.address.model.interview.exceptions.InterviewNotFoundException; +import seedu.address.model.position.Position; + +public class UniqueInterviewList implements Iterable { + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent interview as the given argument. + */ + public boolean contains(Interview toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::equals); + } + + /** + * Returns true if the list contains an interview which has a conflict in timing as the given argument. + */ + public boolean containsConflict(Interview toCheck) { + requireNonNull(toCheck); + for (Interview i : internalList) { + if (i.isConflictingInterview(toCheck)) { + return true; + } + } + return false; + } + + /** + * Adds an interview to the list. + * The interview must not already exist in the list. + */ + public void add(Interview toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateInterviewException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the interview {@code target} in the list with {@code editedInterivew}. + * {@code target} must exist in the list. + * The interview {@code editedInterview} must not be the same as another existing interview in the list. + */ + public void setInterview(Interview target, Interview editedInterview) { + requireAllNonNull(target, editedInterview); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new InterviewNotFoundException(); + } + + if (!target.equals(editedInterview) && contains(editedInterview)) { + throw new DuplicateInterviewException(); + } + + internalList.set(index, editedInterview); + } + + /** + * Removes the equivalent interview from the list. + * The interview must exist in the list. + */ + public void remove(Interview toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new InterviewNotFoundException(); + } + } + + /** + * Returns interview(s) which are for the specified applicant. + */ + public ArrayList getApplicantsInterviews(Applicant applicant) { + ArrayList interviews = new ArrayList<>(); + + for (Interview i : internalList) { + if (i.isInterviewForApplicant(applicant)) { + interviews.add(i); + } + } + + return interviews; + } + + /** + * Returns interview(s) which are for the specified position. + */ + public ArrayList getPositionsInterview(Position position) { + ArrayList interviews = new ArrayList<>(); + + for (Interview i : internalList) { + if (i.isInterviewForPosition(position)) { + interviews.add(i); + } + } + + return interviews; + } + + /** + * Checks if the specified applicant has an interview for the specified position. + */ + public boolean isSameApplicantPosition(Applicant applicant, Position position) { + for (Interview i : getApplicantsInterviews(applicant)) { + if (i.isInterviewForPosition(position)) { + return true; + } + } + return false; + } + + public void setInterviews(UniqueInterviewList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code interview}. + * {@code interview} must not contain duplicate interview. + */ + public void setInterviews(List interview) { + requireAllNonNull(interview); + if (!interviewsAreUnique(interview)) { + throw new DuplicateInterviewException(); + } + + internalList.setAll(interview); + } + + /** + * Sorts a list of interviews + */ + public void sort(Comparator comparator) { + requireNonNull(comparator); + internalList.sort(comparator); + } + + /** + * Updates all interview containing instance of {@code positionToBeUpdated} to {@code newPosition}. + * Effects of editing a Position cascades to update all instances of the old position to the edited position. + */ + public void updatePositions(Position positionToBeUpdated, Position newPosition) { + requireAllNonNull(positionToBeUpdated, newPosition); + for (int i = 0; i < internalList.size(); i++) { + Interview curr = internalList.get(i); + if (curr.getPosition().equals(positionToBeUpdated)) { + internalList.set(i, new Interview(curr.getApplicant(), curr.getDate(), newPosition, curr.getStatus())); + } + } + } + + /** + * Updates all interview containing instance of {@code applicantToBeUpdated} to {@code newApplicant}. + * Effects of editing an Applicant cascades to update all instances of the old applicant to the edited applicant. + */ + public void updateApplicants(Applicant applicantToBeUpdated, Applicant newApplicant) { + requireAllNonNull(applicantToBeUpdated, newApplicant); + for (int i = 0; i < internalList.size(); i++) { + Interview curr = internalList.get(i); + if (curr.getApplicant().equals(applicantToBeUpdated)) { + internalList.set(i, new Interview(newApplicant, curr.getDate(), curr.getPosition(), curr.getStatus())); + } + } + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueInterviewList // instanceof handles nulls + && internalList.equals(((UniqueInterviewList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code interviews} contains only unique interviews. + */ + private boolean interviewsAreUnique(List interviews) { + for (int i = 0; i < interviews.size() - 1; i++) { + for (int j = i + 1; j < interviews.size(); j++) { + if (interviews.get(i).equals(interviews.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/interview/exceptions/DuplicateInterviewException.java b/src/main/java/seedu/address/model/interview/exceptions/DuplicateInterviewException.java new file mode 100644 index 00000000000..db0c60bf3b7 --- /dev/null +++ b/src/main/java/seedu/address/model/interview/exceptions/DuplicateInterviewException.java @@ -0,0 +1,7 @@ +package seedu.address.model.interview.exceptions; + +public class DuplicateInterviewException extends RuntimeException { + public DuplicateInterviewException() { + super("Operation would result in duplicate interviews"); + } +} diff --git a/src/main/java/seedu/address/model/interview/exceptions/InterviewNotFoundException.java b/src/main/java/seedu/address/model/interview/exceptions/InterviewNotFoundException.java new file mode 100644 index 00000000000..e1bd3ab7777 --- /dev/null +++ b/src/main/java/seedu/address/model/interview/exceptions/InterviewNotFoundException.java @@ -0,0 +1,7 @@ +package seedu.address.model.interview.exceptions; + +/** + * Signals that the operation is unable to find the specified Interview. + */ +public class InterviewNotFoundException extends RuntimeException {} + diff --git a/src/main/java/seedu/address/model/interview/exceptions/NonPassableInterviewException.java b/src/main/java/seedu/address/model/interview/exceptions/NonPassableInterviewException.java new file mode 100644 index 00000000000..f8b7915d85f --- /dev/null +++ b/src/main/java/seedu/address/model/interview/exceptions/NonPassableInterviewException.java @@ -0,0 +1,7 @@ +package seedu.address.model.interview.exceptions; + +public class NonPassableInterviewException extends RuntimeException { + public NonPassableInterviewException() { + super("Operation would result in number of extended offers greater than number of position openings"); + } +} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java deleted file mode 100644 index c9b5868427c..00000000000 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ /dev/null @@ -1,31 +0,0 @@ -package seedu.address.model.person; - -import java.util.List; -import java.util.function.Predicate; - -import seedu.address.commons.util.StringUtil; - -/** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. - */ -public class NameContainsKeywordsPredicate implements Predicate { - private final List keywords; - - public NameContainsKeywordsPredicate(List keywords) { - this.keywords = keywords; - } - - @Override - public boolean test(Person person) { - return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof NameContainsKeywordsPredicate // instanceof handles nulls - && keywords.equals(((NameContainsKeywordsPredicate) other).keywords)); // state check - } - -} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java deleted file mode 100644 index 8ff1d83fe89..00000000000 --- a/src/main/java/seedu/address/model/person/Person.java +++ /dev/null @@ -1,123 +0,0 @@ -package seedu.address.model.person; - -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -import seedu.address.model.tag.Tag; - -/** - * Represents a Person in the address book. - * Guarantees: details are present and not null, field values are validated, immutable. - */ -public class Person { - - // Identity fields - private final Name name; - private final Phone phone; - private final Email email; - - // Data fields - private final Address address; - private final Set tags = new HashSet<>(); - - /** - * Every field must be present and not null. - */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - this.tags.addAll(tags); - } - - public Name getName() { - return name; - } - - public Phone getPhone() { - return phone; - } - - public Email getEmail() { - return email; - } - - public Address getAddress() { - return address; - } - - /** - * Returns an immutable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - */ - public Set getTags() { - return Collections.unmodifiableSet(tags); - } - - /** - * Returns true if both persons have the same name. - * This defines a weaker notion of equality between two persons. - */ - public boolean isSamePerson(Person otherPerson) { - if (otherPerson == this) { - return true; - } - - return otherPerson != null - && otherPerson.getName().equals(getName()); - } - - /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. - */ - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - if (!(other instanceof Person)) { - return false; - } - - Person otherPerson = (Person) other; - return otherPerson.getName().equals(getName()) - && otherPerson.getPhone().equals(getPhone()) - && otherPerson.getEmail().equals(getEmail()) - && otherPerson.getAddress().equals(getAddress()) - && otherPerson.getTags().equals(getTags()); - } - - @Override - public int hashCode() { - // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append(getName()) - .append("; Phone: ") - .append(getPhone()) - .append("; Email: ") - .append(getEmail()) - .append("; Address: ") - .append(getAddress()); - - Set tags = getTags(); - if (!tags.isEmpty()) { - builder.append("; Tags: "); - tags.forEach(builder::append); - } - return builder.toString(); - } - -} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java deleted file mode 100644 index 0fee4fe57e6..00000000000 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ /dev/null @@ -1,137 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Iterator; -import java.util.List; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import seedu.address.model.person.exceptions.DuplicatePersonException; -import seedu.address.model.person.exceptions.PersonNotFoundException; - -/** - * A list of persons that enforces uniqueness between its elements and does not allow nulls. - * A person is considered unique by comparing using {@code Person#isSamePerson(Person)}. As such, adding and updating of - * persons uses Person#isSamePerson(Person) for equality so as to ensure that the person being added or updated is - * unique in terms of identity in the UniquePersonList. However, the removal of a person uses Person#equals(Object) so - * as to ensure that the person with exactly the same fields will be removed. - * - * Supports a minimal set of list operations. - * - * @see Person#isSamePerson(Person) - */ -public class UniquePersonList implements Iterable { - - private final ObservableList internalList = FXCollections.observableArrayList(); - private final ObservableList internalUnmodifiableList = - FXCollections.unmodifiableObservableList(internalList); - - /** - * Returns true if the list contains an equivalent person as the given argument. - */ - public boolean contains(Person toCheck) { - requireNonNull(toCheck); - return internalList.stream().anyMatch(toCheck::isSamePerson); - } - - /** - * Adds a person to the list. - * The person must not already exist in the list. - */ - public void add(Person toAdd) { - requireNonNull(toAdd); - if (contains(toAdd)) { - throw new DuplicatePersonException(); - } - internalList.add(toAdd); - } - - /** - * Replaces the person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the list. - * The person identity of {@code editedPerson} must not be the same as another existing person in the list. - */ - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - int index = internalList.indexOf(target); - if (index == -1) { - throw new PersonNotFoundException(); - } - - if (!target.isSamePerson(editedPerson) && contains(editedPerson)) { - throw new DuplicatePersonException(); - } - - internalList.set(index, editedPerson); - } - - /** - * Removes the equivalent person from the list. - * The person must exist in the list. - */ - public void remove(Person toRemove) { - requireNonNull(toRemove); - if (!internalList.remove(toRemove)) { - throw new PersonNotFoundException(); - } - } - - public void setPersons(UniquePersonList replacement) { - requireNonNull(replacement); - internalList.setAll(replacement.internalList); - } - - /** - * Replaces the contents of this list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - requireAllNonNull(persons); - if (!personsAreUnique(persons)) { - throw new DuplicatePersonException(); - } - - internalList.setAll(persons); - } - - /** - * Returns the backing list as an unmodifiable {@code ObservableList}. - */ - public ObservableList asUnmodifiableObservableList() { - return internalUnmodifiableList; - } - - @Override - public Iterator iterator() { - return internalList.iterator(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof UniquePersonList // instanceof handles nulls - && internalList.equals(((UniquePersonList) other).internalList)); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } - - /** - * Returns true if {@code persons} contains only unique persons. - */ - private boolean personsAreUnique(List persons) { - for (int i = 0; i < persons.size() - 1; i++) { - for (int j = i + 1; j < persons.size(); j++) { - if (persons.get(i).isSamePerson(persons.get(j))) { - return false; - } - } - } - return true; - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java deleted file mode 100644 index fa764426ca7..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ /dev/null @@ -1,6 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation is unable to find the specified person. - */ -public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/position/Description.java b/src/main/java/seedu/address/model/position/Description.java new file mode 100644 index 00000000000..b1070585095 --- /dev/null +++ b/src/main/java/seedu/address/model/position/Description.java @@ -0,0 +1,61 @@ +package seedu.address.model.position; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a description of a {@code Position}. Description is a text that briefly describes the nature of the + * position that is available for application. + * Guarantees: immutable; description is valid as declared in {@link #isValidDescriptionText(String)} + */ +public class Description { + + public static final String MESSAGE_CONSTRAINTS = + "Description can contain any characters and spaces, and it should not be blank.\n" + + "Description should contain at least one alphanumeric character (e.g. \"1\" or \"a\")\n" + + "Length of description is restricted to a maximum of 200 characters."; + + /* + * The first character of the description must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + * After the first non-whitespace character, all characters are valid, including newline character. + */ + public static final String VALIDATION_REGEX = "[^\\s][\\s\\S]{0,199}"; + + public final String descriptionText; + + /** + * Constructs a {@code Description}. + * + * @param text A valid description text. + */ + public Description(String text) { + requireNonNull(text); + checkArgument(isValidDescriptionText(text), MESSAGE_CONSTRAINTS); + descriptionText = text; + } + + /** + * Returns true if a given string is a valid description text. + */ + public static boolean isValidDescriptionText(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Description // instanceof handles nulls + && descriptionText.equals(((Description) other).descriptionText)); // state check + } + + @Override + public int hashCode() { + return descriptionText.hashCode(); + } + + @Override + public String toString() { + return descriptionText; + } +} diff --git a/src/main/java/seedu/address/model/position/Position.java b/src/main/java/seedu/address/model/position/Position.java new file mode 100644 index 00000000000..bde97326c64 --- /dev/null +++ b/src/main/java/seedu/address/model/position/Position.java @@ -0,0 +1,251 @@ +package seedu.address.model.position; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import seedu.address.model.position.exceptions.UnableToAcceptOfferException; +import seedu.address.model.position.exceptions.UnableToExtendOfferException; +import seedu.address.model.position.exceptions.UnableToRejectOfferException; + +/** + * Represent a Position in HireLah. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Position { + + public static final String MESSAGE_CONSTRAINTS = + "Position cannot have more offers than openings."; + + // Identity fields + private final PositionName positionName; + private final Description description; + + // Data fields + private final PositionOpenings positionOpenings; + private final PositionOffers positionOffers; + private final Set requirements = new HashSet<>(); + + /** + * Every field must be present, not null and validated. + * Number of offers is left out of parameters. + * Constructor is used for initializing a new position. + */ + public Position(PositionName positionName, Description description, PositionOpenings positionOpenings, + Set requirements) { + requireAllNonNull(positionName, description, positionOpenings, requirements); + this.positionName = positionName; + this.description = description; + this.positionOpenings = positionOpenings; + this.requirements.addAll(requirements); + this.positionOffers = new PositionOffers(); + } + + /** + * Every field must be present, not null and validated. + */ + public Position(PositionName positionName, Description description, PositionOpenings positionOpenings, + PositionOffers positionOffers, Set requirements) { + requireAllNonNull(positionName, description, positionOpenings, positionOffers, requirements); + this.positionName = positionName; + this.description = description; + this.positionOpenings = positionOpenings; + this.requirements.addAll(requirements); + this.positionOffers = positionOffers; + } + + public PositionName getPositionName() { + return positionName; + } + + public Description getDescription() { + return description; + } + + public PositionOpenings getPositionOpenings() { + return positionOpenings; + } + + public PositionOffers getPositionOffers() { + return positionOffers; + } + + /** + * Returns an immutable requirements set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getRequirements() { + return Collections.unmodifiableSet(requirements); + } + + /** + * Returns true if both positions have the same position name. + * This defines a weaker notion of equality between two positions, + * which allows for all positions to have a unique position name. + */ + public boolean isSamePosition(Position otherPosition) { + if (otherPosition == this) { + return true; + } + return otherPosition != null + && otherPosition.positionName.equals(positionName); + } + + /** + * Returns true if number of offers is less than number of openings. + */ + public boolean isValidOpeningsToOffers() { + return positionOffers.getCount() <= positionOpenings.getCount(); + } + + /** + * Returns true if number of offers is less than number of openings. + */ + public boolean canExtendOffer() { + return positionOffers.getCount() < positionOpenings.getCount(); + } + + /** + * Returns true if number of openings and number of offers are more than 0. + */ + public boolean canAcceptOffer() { + return positionOpenings.getCount() > 0 && positionOffers.getCount() > 0; + } + + /** + * Returns true if number of offers is more than 0. + */ + public boolean canRejectOffer() { + return positionOffers.getCount() > 0; + } + + /** + * Returns true if the number of openings is more than 0. + */ + public boolean canScheduleInterview() { + return positionOpenings.getCount() > 0; + } + + /** + * Extends an offer for the current Position. + * An offer can be extended if the current number of offers is less than the current number of openings. + * The new position will contain a number of offers that is 1 more than the previous value. + */ + public Position extendOffer() { + if (!canExtendOffer()) { + throw new UnableToExtendOfferException(); + } + PositionOffers newOfferNumber = positionOffers.increment(); + return new Position(getPositionName(), getDescription(), getPositionOpenings(), newOfferNumber, + getRequirements()); + } + + /** + * Accepts an offer for the current Position. + * An offer can be accepted if the current number of offers is more than 0 and there is more than 0 number of + * openings for the position. + * The new position will contain a number of offers and openings that is 1 less than the previous values. + */ + public Position acceptOffer() { + if (!canAcceptOffer()) { + throw new UnableToAcceptOfferException(); + } + PositionOffers newOfferNumber = positionOffers.decrement(); + PositionOpenings newPositionOpenings = positionOpenings.decrement(); + return new Position(getPositionName(), getDescription(), newPositionOpenings, newOfferNumber, + getRequirements()); + } + + /** + * Rejects an offer for the current Position. + * An offer can be rejected if the current number of offers is more than 0. + * The new position will contain a number of offers that is 1 less than the previous values. + */ + public Position rejectOffer() { + if (!canRejectOffer()) { + throw new UnableToRejectOfferException(); + } + PositionOffers newOfferNumber = positionOffers.decrement(); + return new Position(getPositionName(), getDescription(), getPositionOpenings(), newOfferNumber, + getRequirements()); + } + + /** + * Returns true if both positions have the same identity and data fields. + * This defines a stronger notion of equality between two positions. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Position)) { + return false; + } + + Position otherPosition = (Position) other; + return otherPosition.getPositionName().equals(getPositionName()) + && otherPosition.getDescription().equals(getDescription()) + && otherPosition.getPositionOpenings().equals(getPositionOpenings()) + && otherPosition.getRequirements().equals(getRequirements()) + && otherPosition.getPositionOffers().equals(getPositionOffers()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(positionName, description, positionOpenings, requirements); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getPositionName()) + .append("; Description: ") + .append(getDescription()) + .append("; Openings: ") + .append(getPositionOpenings()) + .append("; Current Offers: ") + .append(getPositionOffers()); + + Set requirements = getRequirements(); + if (!requirements.isEmpty()) { + builder.append("; Requirements: "); + requirements.forEach(builder::append); + } + return builder.toString(); + } + + /** + * Creates csv output for position + */ + public String convertToCsv() { + StringBuilder requirementString = new StringBuilder(); + for (Requirement requirement : requirements) { + requirementString.append(requirement.requirementText); + requirementString.append(" | "); + } + String trimmedRequirementString = requirementString.substring(0, requirementString.length() - 3); + return positionName.positionName + "," + escapeSpecialCharacters(description.descriptionText) + "," + + positionOpenings.toString() + "," + positionOffers.toString() + "," + trimmedRequirementString; + } + + /** + * Eliminates special characters from csv string + */ + //@@author Javi + //Reused from https://stackoverflow.com/questions/61680044/ + // write-elements-of-a-map-to-a-csv-correctly-in-a-simplified-way-in-java-8 + private String escapeSpecialCharacters(String data) { + String escapedData = data.replaceAll("\\R", " "); + if (data.contains(",") || data.contains("\"") || data.contains("'")) { + data = data.replace("\"", "\"\""); + escapedData = "\"" + data + "\""; + } + return escapedData; + } +} diff --git a/src/main/java/seedu/address/model/position/PositionName.java b/src/main/java/seedu/address/model/position/PositionName.java new file mode 100644 index 00000000000..1552b1ef35f --- /dev/null +++ b/src/main/java/seedu/address/model/position/PositionName.java @@ -0,0 +1,64 @@ +package seedu.address.model.position; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Position's name in HireLah. + * Guarantees: immutable; is valid as declared in {@link #isValidPositionName(String)} + */ +public class PositionName { + + public static final String MESSAGE_CONSTRAINTS = + "Position name can contain any characters and spaces, and it should not be blank.\n" + + "Position name should contain at least one alphanumeric character (e.g. \"1\" or \"a\")\n" + + "Length of position name is restricted to a maximum of 100 characters."; + + /* + * The first character of the position must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "(?=.*[\\p{Alnum}].*)[^\\s].{0,99}"; + + public final String positionName; + + /** + * Constructs a {@code PositionName}. + * + * @param name A valid and unique position name. + */ + public PositionName(String name) { + requireNonNull(name); + checkArgument(isValidPositionName(name), MESSAGE_CONSTRAINTS); + positionName = name; + } + + /** + * Returns true if a given string is a valid position name. + * + * @param test + * @return + */ + public static boolean isValidPositionName(String test) { + return test.matches(VALIDATION_REGEX); + } + + + @Override + public String toString() { + return positionName; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PositionName // instanceof handles nulls + && positionName.equals(((PositionName) other).positionName)); // state check + } + + @Override + public int hashCode() { + return positionName.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/position/PositionNameComparator.java b/src/main/java/seedu/address/model/position/PositionNameComparator.java new file mode 100644 index 00000000000..33b5749a350 --- /dev/null +++ b/src/main/java/seedu/address/model/position/PositionNameComparator.java @@ -0,0 +1,20 @@ +package seedu.address.model.position; + +import java.util.Comparator; + +public class PositionNameComparator implements Comparator { + private final String sortArgument; + + public PositionNameComparator(String sortArgument) { + this.sortArgument = sortArgument; + } + + @Override + public int compare(Position o1, Position o2) { + if (sortArgument.equals("asc")) { + return o1.getPositionName().positionName.compareTo(o2.getPositionName().positionName); + } else { + return o2.getPositionName().positionName.compareTo(o1.getPositionName().positionName); + } + } +} diff --git a/src/main/java/seedu/address/model/position/PositionNamePredicate.java b/src/main/java/seedu/address/model/position/PositionNamePredicate.java new file mode 100644 index 00000000000..f81e53e851d --- /dev/null +++ b/src/main/java/seedu/address/model/position/PositionNamePredicate.java @@ -0,0 +1,30 @@ +package seedu.address.model.position; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Position}'s {@code Name} matches any of the keywords given. + */ +public class PositionNamePredicate implements Predicate { + private final List keywords; + + public PositionNamePredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Position pos) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(pos.getPositionName().positionName, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PositionNamePredicate // instanceof handles nulls + && keywords.equals(((PositionNamePredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/position/PositionOffers.java b/src/main/java/seedu/address/model/position/PositionOffers.java new file mode 100644 index 00000000000..592018d5744 --- /dev/null +++ b/src/main/java/seedu/address/model/position/PositionOffers.java @@ -0,0 +1,76 @@ +package seedu.address.model.position; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.address.model.position.Position.MESSAGE_CONSTRAINTS; +import static seedu.address.model.position.PositionOpenings.isValidNumber; + +import seedu.address.model.ImmutableCounter; + +/** + * Represents the number of offers for a Position in HireLah. + * Guarantees: immutable; is always initialized as 0 {@link #PositionOffers()} + */ +public class PositionOffers implements ImmutableCounter { + + private Integer numOfOffers; + + /** + * Constructs a PositionOffers with a count of 0 + */ + public PositionOffers() { + numOfOffers = 0; + } + + /** + * Constructs a {@code PositionOffers} + * Constructor is used internally to increment or decrement the counter + */ + private PositionOffers(Integer offers) { + requireNonNull(offers); + numOfOffers = offers; + } + + /** + * Constructs a {@code PositionOpenings} + * @param offers A valid non-negative integer that is between 1 and 3 digits + */ + public PositionOffers(String offers) { + requireNonNull(offers); + checkArgument(isValidNumber(offers), MESSAGE_CONSTRAINTS); + numOfOffers = Integer.parseInt(offers); + } + + @Override + public PositionOffers increment() { + return new PositionOffers(numOfOffers + 1); + } + + @Override + public PositionOffers decrement() { + assert numOfOffers > 0; + return new PositionOffers(numOfOffers - 1); + } + + @Override + public Integer getCount() { + return numOfOffers; + } + + @Override + public String toString() { + return numOfOffers.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PositionOffers // instanceof handles nulls + && numOfOffers.equals(((PositionOffers) other).numOfOffers)); // state check + } + + @Override + public int hashCode() { + return numOfOffers.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/position/PositionOpenings.java b/src/main/java/seedu/address/model/position/PositionOpenings.java new file mode 100644 index 00000000000..98b0aabf525 --- /dev/null +++ b/src/main/java/seedu/address/model/position/PositionOpenings.java @@ -0,0 +1,84 @@ +package seedu.address.model.position; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import seedu.address.model.ImmutableCounter; + +/** + * Represents the number of openings in a Position in HireLah. + * Guarantees: is non-negative + */ +public class PositionOpenings implements ImmutableCounter { + + public static final String MESSAGE_CONSTRAINTS = + "Number of openings should only contain numbers, and it should be between 1 to 5 digits.\n" + + "Number must not start with a 0."; + + /* + * Number of openings must be a valid integer with 1 to 5 digits. + */ + public static final String VALIDATION_REGEX = "\\d{1,5}"; + + private Integer numOfOpenings; + + /** + * Constructs a {@code PositionOpenings} + * + * @param openings A valid non-negative integer that is between 1 and 3 digits. + */ + public PositionOpenings(String openings) { + requireNonNull(openings); + checkArgument(isValidNumber(openings), MESSAGE_CONSTRAINTS); + numOfOpenings = Integer.parseInt(openings); + } + + /** + * Constructs a {@code PositionOpenings} + * Constructor is used internally to increment or decrement the counter + */ + private PositionOpenings(Integer openings) { + requireNonNull(openings); + numOfOpenings = openings; + } + + /** + * Returns true if a given string is a valid number for openings. + */ + public static boolean isValidNumber(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public PositionOpenings increment() { + return new PositionOpenings(numOfOpenings + 1); + } + + @Override + public PositionOpenings decrement() { + assert numOfOpenings > 0; + return new PositionOpenings(numOfOpenings - 1); + } + + @Override + public Integer getCount() { + return numOfOpenings; + } + + @Override + public String toString() { + return numOfOpenings.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PositionOpenings // instanceof handles nulls + && numOfOpenings.equals(((PositionOpenings) other).numOfOpenings)); // state check + } + + @Override + public int hashCode() { + return numOfOpenings.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/position/PositionRequirementPredicate.java b/src/main/java/seedu/address/model/position/PositionRequirementPredicate.java new file mode 100644 index 00000000000..f46b3e8dbd5 --- /dev/null +++ b/src/main/java/seedu/address/model/position/PositionRequirementPredicate.java @@ -0,0 +1,35 @@ +package seedu.address.model.position; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Position} has a {@code Requirement} that matches any of the keywords given. + */ +public class PositionRequirementPredicate implements Predicate { + private final List keywords; + + public PositionRequirementPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Position position) { + for (Requirement req : position.getRequirements()) { + if (keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(req.requirementText, keyword))) { + return true; + } + } + return false; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PositionRequirementPredicate // instanceof handles nulls + && keywords.equals(((PositionRequirementPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/position/Requirement.java b/src/main/java/seedu/address/model/position/Requirement.java new file mode 100644 index 00000000000..2aaa1e13f2f --- /dev/null +++ b/src/main/java/seedu/address/model/position/Requirement.java @@ -0,0 +1,60 @@ +package seedu.address.model.position; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Requirement in the address book. Requirement can denote a skill/experience that a candidate needs for + * a job position. + * Guarantees: immutable; requirement is valid as declared in {@link #isValidRequirementText(String)} + */ +public class Requirement { + + public static final String MESSAGE_CONSTRAINTS = + "Requirement text can contain any characters and spaces, and it should not be blank.\n" + + "Requirement text should contain at least one alphanumeric character (e.g. \"1\" or \"a\")\n" + + "Length of requirement text is restricted to a maximum of 30 characters.\n" + + "If you need to fit more information, consider splitting information over multiple requirements."; + + public static final String VALIDATION_REGEX = "(?=.*[\\p{Alnum}].*)([^\\s].{0,29})"; + + public final String requirementText; + + /** + * Constructs a {@code Requirement}. + * + * @param requirementText A valid requirement text. + */ + public Requirement(String requirementText) { + requireNonNull(requirementText); + checkArgument(isValidRequirementText(requirementText), MESSAGE_CONSTRAINTS); + this.requirementText = requirementText; + } + + /** + * Returns true if a given string is a valid requirement text. + */ + public static boolean isValidRequirementText(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Requirement // instanceof handles nulls + && requirementText.equals(((Requirement) other).requirementText)); // state check + } + + @Override + public int hashCode() { + return requirementText.hashCode(); + } + + /** + * Format requirement as text for viewing. + */ + @Override + public String toString() { + return '[' + requirementText + ']'; + } +} diff --git a/src/main/java/seedu/address/model/position/UniquePositionList.java b/src/main/java/seedu/address/model/position/UniquePositionList.java new file mode 100644 index 00000000000..817dc5ea769 --- /dev/null +++ b/src/main/java/seedu/address/model/position/UniquePositionList.java @@ -0,0 +1,154 @@ +package seedu.address.model.position; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.position.exceptions.DuplicatePositionException; +import seedu.address.model.position.exceptions.PositionNotFoundException; + +/** + * A list of positions that enforces uniqueness between its elements and down not allow nulls. + * A position is considered unique by comparing using {@code Position#isSamePosition(Position)}. As such, adding and + * updating of positions uses Position#isSamePosition(Position) for equality so as to ensure that the Position being + * added or updated is unique in terms of identity in the UniquePositionList. However, the removal of a position uses + * Position#equals(Object) so as to ensure that the position with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + * @see Position#isSamePosition(Position) + */ +public class UniquePositionList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent position as the given argument. + */ + public boolean contains(Position toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSamePosition); + } + + /** + * Adds a position to the list. + * The position must not already exist in the list. + */ + public void add(Position toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicatePositionException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the position {@code target} in the list with {@code editedPosition}. + * {@code target} must exist in the list. + * The position identity of {@code editedPosition} must not be the same as another existing position in the list. + */ + public void setPosition(Position target, Position editedPosition) { + requireAllNonNull(target, editedPosition); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new PositionNotFoundException(); + } + + if (!target.isSamePosition(editedPosition) && contains(editedPosition)) { + throw new DuplicatePositionException(); + } + + internalList.set(index, editedPosition); + } + + /** + * Removes the equivalent position from the list. + * The position must exist in the list. + */ + public void remove(Position toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new PositionNotFoundException(); + } + } + + public void setPositions(UniquePositionList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code positions}. + * {@code positions} must not contain duplicate positions. + */ + public void setPositions(List positions) { + requireAllNonNull(positions); + if (!positionsAreUnique(positions)) { + throw new DuplicatePositionException(); + } + + internalList.setAll(positions); + } + + /** + * Sorts a list of positions + */ + public void sort(Comparator comparator) { + requireNonNull(comparator); + internalList.sort(comparator); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniquePositionList // instanceof handles nulls + && internalList.equals(((UniquePositionList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code positions} contains only unique positions. + * @param positions + * @return + */ + private boolean positionsAreUnique(List positions) { + for (int i = 0; i < positions.size() - 1; i++) { + for (int j = i + 1; j < positions.size(); j++) { + if (positions.get(i).isSamePosition(positions.get(j))) { + return false; + } + } + } + return true; + } + // May change isSamePosition to equals if required + public Position getPosition(Position interviewPosition) { + return internalList.stream().filter(a -> a.isSamePosition(interviewPosition)) + .collect(Collectors.toList()).get(0); + } +} diff --git a/src/main/java/seedu/address/model/position/exceptions/DuplicatePositionException.java b/src/main/java/seedu/address/model/position/exceptions/DuplicatePositionException.java new file mode 100644 index 00000000000..6613fa799d7 --- /dev/null +++ b/src/main/java/seedu/address/model/position/exceptions/DuplicatePositionException.java @@ -0,0 +1,11 @@ +package seedu.address.model.position.exceptions; + +/** + * Signals that the operation will result in duplicate Positions (Positions are considered duplicates if they have the + * same identity). + */ +public class DuplicatePositionException extends RuntimeException { + public DuplicatePositionException() { + super("Operation would result in duplicate positions"); + } +} diff --git a/src/main/java/seedu/address/model/position/exceptions/PositionNotFoundException.java b/src/main/java/seedu/address/model/position/exceptions/PositionNotFoundException.java new file mode 100644 index 00000000000..f5ba217a983 --- /dev/null +++ b/src/main/java/seedu/address/model/position/exceptions/PositionNotFoundException.java @@ -0,0 +1,10 @@ +package seedu.address.model.position.exceptions; + +/** + * Signals that the operation is unable to find the specified person. + */ +public class PositionNotFoundException extends RuntimeException { + public PositionNotFoundException() { + super("Position is not found in the position list"); + } +} diff --git a/src/main/java/seedu/address/model/position/exceptions/UnableToAcceptOfferException.java b/src/main/java/seedu/address/model/position/exceptions/UnableToAcceptOfferException.java new file mode 100644 index 00000000000..abecae5975b --- /dev/null +++ b/src/main/java/seedu/address/model/position/exceptions/UnableToAcceptOfferException.java @@ -0,0 +1,7 @@ +package seedu.address.model.position.exceptions; + +public class UnableToAcceptOfferException extends RuntimeException { + public UnableToAcceptOfferException() { + super("there are no outstanding offer in the position"); + } +} diff --git a/src/main/java/seedu/address/model/position/exceptions/UnableToExtendOfferException.java b/src/main/java/seedu/address/model/position/exceptions/UnableToExtendOfferException.java new file mode 100644 index 00000000000..f1a91947cea --- /dev/null +++ b/src/main/java/seedu/address/model/position/exceptions/UnableToExtendOfferException.java @@ -0,0 +1,7 @@ +package seedu.address.model.position.exceptions; + +public class UnableToExtendOfferException extends RuntimeException { + public UnableToExtendOfferException() { + super("Number of offers has exceed the number of openings in the position"); + } +} diff --git a/src/main/java/seedu/address/model/position/exceptions/UnableToRejectOfferException.java b/src/main/java/seedu/address/model/position/exceptions/UnableToRejectOfferException.java new file mode 100644 index 00000000000..70b22eb87cf --- /dev/null +++ b/src/main/java/seedu/address/model/position/exceptions/UnableToRejectOfferException.java @@ -0,0 +1,7 @@ +package seedu.address.model.position.exceptions; + +public class UnableToRejectOfferException extends RuntimeException { + public UnableToRejectOfferException() { + super("Number of offers is 0"); + } +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index b0ea7e7dad7..49ffb302404 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -9,8 +9,11 @@ */ 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 = "Tag's text should contain at least 1 alphanumeric character " + + "and contain 30 or less characters. If you need to fit more information, consider breaking " + + "them up into different tags."; + public static final int MAX_TEXT_LENGTH = 30; + public static final String VALIDATION_REGEX = "(?=.*[\\p{Alnum}].*)([^\\s].*)"; public final String tagName; @@ -29,7 +32,7 @@ public Tag(String tagName) { * Returns true if a given string is a valid tag name. */ public static boolean isValidTagName(String test) { - return test.matches(VALIDATION_REGEX); + return test.matches(VALIDATION_REGEX) && test.length() <= MAX_TEXT_LENGTH; } @Override diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..c1f12c33775 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,49 +1,108 @@ package seedu.address.model.util; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; +import seedu.address.model.HireLah; +import seedu.address.model.ReadOnlyHireLah; +import seedu.address.model.applicant.Address; +import seedu.address.model.applicant.Age; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.applicant.Email; +import seedu.address.model.applicant.Gender; +import seedu.address.model.applicant.Name; +import seedu.address.model.applicant.Phone; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Description; +import seedu.address.model.position.Position; +import seedu.address.model.position.PositionName; +import seedu.address.model.position.PositionOffers; +import seedu.address.model.position.PositionOpenings; +import seedu.address.model.position.Requirement; import seedu.address.model.tag.Tag; /** - * Contains utility methods for populating {@code AddressBook} with sample data. + * Contains utility methods for populating {@code HireLah} with sample data. */ public class SampleDataUtil { - public static Person[] getSamplePersons() { - return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + public static Applicant[] getSampleApplicants() { + return new Applicant[]{ + new Applicant(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), + new Age("21"), new Address("Blk 30 Geylang Street 29, #06-40"), new Gender("M"), + getTagSet(">=5 years of exp", "1st Class Honours", "Computer Science")), + new Applicant(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), + new Age("22"), new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), new Gender("F"), + getTagSet("Marketing", "Referred by manager")), + new Applicant(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), + new Age("23"), new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), new Gender("F"), + getTagSet("Ex-Google")), + new Applicant(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), + new Age("24"), new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), new Gender("M"), + getTagSet("NUS Alumni", "National Athlete")), + new Applicant(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), + new Age("25"), new Address("Blk 47 Tampines Street 20, #17-35"), new Gender("M"), + getTagSet("Ex Senior Engineer @ Amazon")), + new Applicant(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), + new Age("26"), new Address("Blk 45 Aljunied Street 85, #11-31"), new Gender("M"), + getTagSet("ICPC Winner")) }; } - public static ReadOnlyAddressBook getSampleAddressBook() { - AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); + public static Position[] getSamplePositions() { + PositionOffers samplePos = new PositionOffers(); + samplePos.increment(); + return new Position[]{ + new Position(new PositionName("Senior Software Developer"), + new Description("The highest paying job in the company. More than 5 years experience."), + new PositionOpenings("3"), getRequirementSet("Java", "C++")), + new Position(new PositionName("IT Intern"), + new Description("Work on internal tools. Must be willing to learn."), + new PositionOpenings("1"), + getRequirementSet("Source Academy", "C")), + new Position(new PositionName("Janitor"), + new Description("Arguably the most important job"), + new PositionOpenings("2"), + getRequirementSet("Sweep Floor", "Wipe Window", "Wash Toilet")), + new Position(new PositionName("Admin Officer"), + new Description("Degree or Postgraduate holder with Major in Information Technology, " + + "Computer Science, or other similar areas, and a cumulative GPA of 3.5 and above."), + new PositionOpenings("2"), samplePos, getRequirementSet("Hardworking", "Good with people")) + }; + } + + public static Interview[] getSampleInterviews() { + Applicant[] samplePersons = getSampleApplicants(); + Position[] samplePositions = getSamplePositions(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + return new Interview[]{ + new Interview(samplePersons[0], + LocalDateTime.parse("2021-01-01 12:00", formatter), + samplePositions[0]), + new Interview(samplePersons[1], + LocalDateTime.parse("2021-05-05 05:00", formatter), + samplePositions[1]), + new Interview(samplePersons[2], + LocalDateTime.parse("2021-09-09 18:00", formatter), + samplePositions[2]), + new Interview(samplePersons[3], + LocalDateTime.parse("2021-12-20 19:00", formatter), + samplePositions[3]) + }; + } + + public static ReadOnlyHireLah getSampleHireLah() { + HireLah sampleAb = new HireLah(); + for (Applicant sampleApplicant : getSampleApplicants()) { + sampleAb.addApplicant(sampleApplicant); + } + for (Position samplePosition : getSamplePositions()) { + sampleAb.addPosition(samplePosition); + } + for (Interview sampleInterview : getSampleInterviews()) { + sampleAb.addInterview(sampleInterview); } return sampleAb; } @@ -57,4 +116,12 @@ public static Set getTagSet(String... strings) { .collect(Collectors.toSet()); } + /** + * Returns a requirement set containing the list of strings given. + */ + public static Set getRequirementSet(String... strings) { + return Arrays.stream(strings) + .map(Requirement::new) + .collect(Collectors.toSet()); + } } diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/seedu/address/storage/AddressBookStorage.java deleted file mode 100644 index 4599182b3f9..00000000000 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ /dev/null @@ -1,45 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * Represents a storage for {@link seedu.address.model.AddressBook}. - */ -public interface AddressBookStorage { - - /** - * Returns the file path of the data file. - */ - Path getAddressBookFilePath(); - - /** - * Returns AddressBook data as a {@link ReadOnlyAddressBook}. - * Returns {@code Optional.empty()} if storage file is not found. - * @throws DataConversionException if the data in storage is not in the expected format. - * @throws IOException if there was any problem when reading from the storage. - */ - Optional readAddressBook() throws DataConversionException, IOException; - - /** - * @see #getAddressBookFilePath() - */ - Optional readAddressBook(Path filePath) throws DataConversionException, IOException; - - /** - * Saves the given {@link ReadOnlyAddressBook} to the storage. - * @param addressBook cannot be null. - * @throws IOException if there was any problem writing to the file. - */ - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - - /** - * @see #saveAddressBook(ReadOnlyAddressBook) - */ - void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/HireLahStorage.java b/src/main/java/seedu/address/storage/HireLahStorage.java new file mode 100644 index 00000000000..0c59c0c03af --- /dev/null +++ b/src/main/java/seedu/address/storage/HireLahStorage.java @@ -0,0 +1,46 @@ +package seedu.address.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.model.HireLah; +import seedu.address.model.ReadOnlyHireLah; + +/** + * Represents a storage for {@link HireLah}. + */ +public interface HireLahStorage { + + /** + * Returns the file path of the data file. + */ + Path getHireLahFilePath(); + + /** + * Returns HireLah data as a {@link ReadOnlyHireLah}. + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readHireLah() throws DataConversionException, IOException; + + /** + * @see #getHireLahFilePath() + */ + Optional readHireLah(Path filePath) throws DataConversionException, IOException; + + /** + * Saves the given {@link ReadOnlyHireLah} to the storage. + * @param hireLah cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveHireLah(ReadOnlyHireLah hireLah) throws IOException; + + /** + * @see #saveHireLah(ReadOnlyHireLah) + */ + void saveHireLah(ReadOnlyHireLah hireLah, Path filePath) throws IOException; + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedApplicant.java similarity index 51% rename from src/main/java/seedu/address/storage/JsonAdaptedPerson.java rename to src/main/java/seedu/address/storage/JsonAdaptedApplicant.java index a6321cec2ea..da324fcf661 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedApplicant.java @@ -10,61 +10,75 @@ import com.fasterxml.jackson.annotation.JsonProperty; import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; +import seedu.address.model.applicant.Address; +import seedu.address.model.applicant.Age; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.applicant.Email; +import seedu.address.model.applicant.Gender; +import seedu.address.model.applicant.HiredStatus; +import seedu.address.model.applicant.Name; +import seedu.address.model.applicant.Phone; import seedu.address.model.tag.Tag; /** - * Jackson-friendly version of {@link Person}. + * Jackson-friendly version of {@link Applicant}. */ -class JsonAdaptedPerson { +class JsonAdaptedApplicant { - public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Applicant's %s field is missing!"; private final String name; private final String phone; private final String email; + private final String age; private final String address; + private final String gender; + private final String hiredStatus; private final List tagged = new ArrayList<>(); /** - * Constructs a {@code JsonAdaptedPerson} with the given person details. + * Constructs a {@code JsonAdaptedPerson} with the given applicant details. */ @JsonCreator - public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tagged") List tagged) { + public JsonAdaptedApplicant(@JsonProperty("name") String name, @JsonProperty("phone") String phone, + @JsonProperty("email") String email, @JsonProperty("age") String age, + @JsonProperty("address") String address, @JsonProperty("gender") String gender, + @JsonProperty("tagged") List tagged, @JsonProperty("hiredStatus") String hiredStatus) { this.name = name; this.phone = phone; this.email = email; + this.age = age; this.address = address; + this.gender = gender; + this.hiredStatus = hiredStatus; + if (tagged != null) { this.tagged.addAll(tagged); } } /** - * Converts a given {@code Person} into this class for Jackson use. + * Converts a given {@code Applicant} into this class for Jackson use. */ - public JsonAdaptedPerson(Person source) { + public JsonAdaptedApplicant(Applicant source) { name = source.getName().fullName; phone = source.getPhone().value; email = source.getEmail().value; + age = source.getAge().value; address = source.getAddress().value; + gender = source.getGender().value; + hiredStatus = source.getStatus().getValue(); tagged.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); } /** - * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. + * Converts this Jackson-friendly adapted applicant object into the model's {@code Applicant} object. * - * @throws IllegalValueException if there were any data constraints violated in the adapted person. + * @throws IllegalValueException if there were any data constraints violated in the adapted applicant. */ - public Person toModelType() throws IllegalValueException { + public Applicant toModelType() throws IllegalValueException { final List personTags = new ArrayList<>(); for (JsonAdaptedTag tag : tagged) { personTags.add(tag.toModelType()); @@ -94,6 +108,14 @@ public Person toModelType() throws IllegalValueException { } final Email modelEmail = new Email(email); + if (age == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Age.class.getSimpleName())); + } + if (!Age.isValidAge(age)) { + throw new IllegalValueException(Age.MESSAGE_CONSTRAINTS); + } + final Age modelAge = new Age(age); + if (address == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); } @@ -102,8 +124,24 @@ public Person toModelType() throws IllegalValueException { } final Address modelAddress = new Address(address); + if (gender == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Gender.class.getSimpleName())); + } + if (!Gender.isValidGender(gender)) { + throw new IllegalValueException(Gender.MESSAGE_CONSTRAINTS); + } + final Gender modelGender = new Gender(gender); + + if (hiredStatus == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + HiredStatus.class.getSimpleName())); + } + final HiredStatus modelHireStatus = new HiredStatus(hiredStatus); + final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + + return new Applicant(modelName, modelPhone, modelEmail, modelAge, modelAddress, modelGender, modelHireStatus, + modelTags); } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedInterview.java b/src/main/java/seedu/address/storage/JsonAdaptedInterview.java new file mode 100644 index 00000000000..cfd3c58400d --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedInterview.java @@ -0,0 +1,78 @@ +package seedu.address.storage; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.interview.Interview; +import seedu.address.model.interview.Status; +import seedu.address.model.position.Position; + +public class JsonAdaptedInterview { + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Interview's %s field is missing!"; + + private final JsonAdaptedApplicant applicant; + private final LocalDateTime date; + private final JsonAdaptedPosition position; + private String status; + + /** + * Constructs a {@code JsonAdaptedInterview} with the given interview details. + */ + @JsonCreator + public JsonAdaptedInterview(@JsonProperty("applicant") JsonAdaptedApplicant applicant, + @JsonProperty("date") LocalDateTime date, @JsonProperty("position") JsonAdaptedPosition position, + @JsonProperty("status") String status) { + this.applicant = applicant; + this.date = date; + this.position = position; + this.status = status; + } + + /** + * Converts a given {@code Interview} into this class for Jackson use. + */ + public JsonAdaptedInterview(Interview source) { + applicant = new JsonAdaptedApplicant(source.getApplicant()); + date = source.getDate(); + position = new JsonAdaptedPosition(source.getPosition()); + status = source.getStatus().toString(); + } + + /** + * Converts this Jackson-friendly adapted interview object into the model's {@code Interview} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted interview. + */ + public Interview toModelType() throws IllegalValueException { + if (applicant == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + JsonAdaptedApplicant.class.getSimpleName())); + } + final Applicant modelApplicant = applicant.toModelType(); + + if (date == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + LocalDateTime.class.getSimpleName())); + } + final LocalDateTime modelDate = date; + + if (position == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + JsonAdaptedPosition.class.getSimpleName())); + } + final Position modelPosition = position.toModelType(); + + if (status == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Status.class.getSimpleName())); + } + + final Status status = new Status(this.status); + + return new Interview(modelApplicant, modelDate, modelPosition, status); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPosition.java b/src/main/java/seedu/address/storage/JsonAdaptedPosition.java new file mode 100644 index 00000000000..3dbd411d897 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedPosition.java @@ -0,0 +1,109 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.position.Description; +import seedu.address.model.position.Position; +import seedu.address.model.position.PositionName; +import seedu.address.model.position.PositionOffers; +import seedu.address.model.position.PositionOpenings; +import seedu.address.model.position.Requirement; + +public class JsonAdaptedPosition { + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Position's %s field is missing!"; + + private final String positionName; + private final String description; + private final String positionOpening; + private final List requirements = new ArrayList<>(); + private final String positionOffers; + + /** + * Constructs a {@code JsonAdaptedPosition} with the given position details. + */ + @JsonCreator + public JsonAdaptedPosition(@JsonProperty("positionName") String positionName, + @JsonProperty("description") String description, @JsonProperty("positionOpening") String positionOpening, + @JsonProperty("requirements") List requirements, + @JsonProperty("positionOffers") String positionOffers) { + this.positionName = positionName; + this.description = description; + this.positionOpening = positionOpening; + this.positionOffers = positionOffers; + + if (requirements != null) { + this.requirements.addAll(requirements); + } + } + + /** + * Converts a given {@code Position} into this class for Jackson use. + */ + public JsonAdaptedPosition(Position source) { + positionName = source.getPositionName().positionName; + description = source.getDescription().descriptionText; + positionOpening = source.getPositionOpenings().toString(); + positionOffers = source.getPositionOffers().toString(); + + requirements.addAll(source.getRequirements().stream() + .map(JsonAdaptedRequirement::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted position object into the model's {@code Position} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted position. + */ + public Position toModelType() throws IllegalValueException { + final List positionRequirements = new ArrayList<>(); + for (JsonAdaptedRequirement requirement: requirements) { + positionRequirements.add(requirement.toModelType()); + } + if (positionName == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + PositionName.class.getSimpleName())); + } + if (!PositionName.isValidPositionName(positionName)) { + throw new IllegalValueException(PositionName.MESSAGE_CONSTRAINTS); + } + final PositionName modelPositionName = new PositionName(positionName); + + if (description == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Description.class.getSimpleName())); + } + if (!Description.isValidDescriptionText(description)) { + throw new IllegalValueException(Description.MESSAGE_CONSTRAINTS); + } + final Description modelDescription = new Description(description); + + if (positionOpening == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + PositionOpenings.class.getSimpleName())); + } + if (!PositionOpenings.isValidNumber(positionOpening)) { + throw new IllegalValueException(PositionOpenings.MESSAGE_CONSTRAINTS); + } + final PositionOpenings modelPositionOpenings = new PositionOpenings(positionOpening); + + if (positionOffers == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + PositionOffers.class.getSimpleName())); + } + final PositionOffers modelPositionOffers = new PositionOffers(positionOffers); + + final Set modelRequirement = new HashSet<>(positionRequirements); + + return new Position(modelPositionName, modelDescription, modelPositionOpenings, modelPositionOffers, + modelRequirement); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedRequirement.java b/src/main/java/seedu/address/storage/JsonAdaptedRequirement.java new file mode 100644 index 00000000000..f6966271d5a --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedRequirement.java @@ -0,0 +1,46 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.position.Requirement; + + +public class JsonAdaptedRequirement { + + private final String requirementText; + + /** + * Constructs a {@code JsonAdaptedRequirement} with the given {@code requirementText}. + */ + @JsonCreator + public JsonAdaptedRequirement(String requirementText) { + this.requirementText = requirementText; + } + + /** + * Converts a given {@code Requirement} into this class for Jackson use. + */ + public JsonAdaptedRequirement(Requirement source) { + requirementText = source.requirementText; + } + + @JsonValue + public String getRequirementText() { + return requirementText; + } + + /** + * Converts this Jackson-friendly adapted tag object into the model's {@code Requirement} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted tag. + */ + public Requirement toModelType() throws IllegalValueException { + if (!Requirement.isValidRequirementText(requirementText)) { + throw new IllegalValueException(Requirement.MESSAGE_CONSTRAINTS); + } + return new Requirement(requirementText); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/address/storage/JsonAdaptedTag.java index 0df22bdb754..4e74990a4cd 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedTag.java @@ -10,7 +10,7 @@ * Jackson-friendly version of {@link Tag}. */ class JsonAdaptedTag { - + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Tag's %s field is missing!"; private final String tagName; /** diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/address/storage/JsonHireLahStorage.java similarity index 54% rename from src/main/java/seedu/address/storage/JsonAddressBookStorage.java rename to src/main/java/seedu/address/storage/JsonHireLahStorage.java index dfab9daaa0d..5cbb519251c 100644 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ b/src/main/java/seedu/address/storage/JsonHireLahStorage.java @@ -12,45 +12,44 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.commons.util.FileUtil; import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyHireLah; /** - * A class to access AddressBook data stored as a json file on the hard disk. + * A class to access HireLah data stored as a json file on the hard disk. */ -public class JsonAddressBookStorage implements AddressBookStorage { +public class JsonHireLahStorage implements HireLahStorage { - private static final Logger logger = LogsCenter.getLogger(JsonAddressBookStorage.class); + private static final Logger logger = LogsCenter.getLogger(JsonHireLahStorage.class); private Path filePath; - public JsonAddressBookStorage(Path filePath) { + public JsonHireLahStorage(Path filePath) { this.filePath = filePath; } - public Path getAddressBookFilePath() { + public Path getHireLahFilePath() { return filePath; } @Override - public Optional readAddressBook() throws DataConversionException { - return readAddressBook(filePath); + public Optional readHireLah() throws DataConversionException { + return readHireLah(filePath); } /** - * Similar to {@link #readAddressBook()}. + * Similar to {@link #readHireLah()}. * * @param filePath location of the data. Cannot be null. * @throws DataConversionException if the file is not in the correct format. */ - public Optional readAddressBook(Path filePath) throws DataConversionException { + public Optional readHireLah(Path filePath) throws DataConversionException { requireNonNull(filePath); - Optional jsonAddressBook = JsonUtil.readJsonFile( - filePath, JsonSerializableAddressBook.class); + Optional jsonAddressBook = JsonUtil.readJsonFile( + filePath, JsonSerializableHireLah.class); if (!jsonAddressBook.isPresent()) { return Optional.empty(); } - try { return Optional.of(jsonAddressBook.get().toModelType()); } catch (IllegalValueException ive) { @@ -60,21 +59,21 @@ public Optional readAddressBook(Path filePath) throws DataC } @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, filePath); + public void saveHireLah(ReadOnlyHireLah hireLah) throws IOException { + saveHireLah(hireLah, filePath); } /** - * Similar to {@link #saveAddressBook(ReadOnlyAddressBook)}. + * Similar to {@link #saveHireLah(ReadOnlyHireLah)}. * * @param filePath location of the data. Cannot be null. */ - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - requireNonNull(addressBook); + public void saveHireLah(ReadOnlyHireLah hireLah, Path filePath) throws IOException { + requireNonNull(hireLah); requireNonNull(filePath); FileUtil.createIfMissing(filePath); - JsonUtil.saveJsonFile(new JsonSerializableAddressBook(addressBook), filePath); + JsonUtil.saveJsonFile(new JsonSerializableHireLah(hireLah), filePath); } } diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java deleted file mode 100644 index 5efd834091d..00000000000 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * An Immutable AddressBook that is serializable to JSON format. - */ -@JsonRootName(value = "addressbook") -class JsonSerializableAddressBook { - - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; - - private final List persons = new ArrayList<>(); - - /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. - */ - @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { - this.persons.addAll(persons); - } - - /** - * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. - * - * @param source future changes to this will not affect the created {@code JsonSerializableAddressBook}. - */ - public JsonSerializableAddressBook(ReadOnlyAddressBook source) { - persons.addAll(source.getPersonList().stream().map(JsonAdaptedPerson::new).collect(Collectors.toList())); - } - - /** - * Converts this address book into the model's {@code AddressBook} object. - * - * @throws IllegalValueException if there were any data constraints violated. - */ - public AddressBook toModelType() throws IllegalValueException { - AddressBook addressBook = new AddressBook(); - for (JsonAdaptedPerson jsonAdaptedPerson : persons) { - Person person = jsonAdaptedPerson.toModelType(); - if (addressBook.hasPerson(person)) { - throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); - } - addressBook.addPerson(person); - } - return addressBook; - } - -} diff --git a/src/main/java/seedu/address/storage/JsonSerializableHireLah.java b/src/main/java/seedu/address/storage/JsonSerializableHireLah.java new file mode 100644 index 00000000000..78de4304261 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonSerializableHireLah.java @@ -0,0 +1,103 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.HireLah; +import seedu.address.model.ReadOnlyHireLah; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.interview.Interview; +import seedu.address.model.position.Position; + +/** + * An Immutable HireLah that is serializable to JSON format. + */ +@JsonRootName(value = "HireLah") +class JsonSerializableHireLah { + + public static final String MESSAGE_DUPLICATE_APPLICANT = "Applicants list contains duplicate applicant(s)."; + public static final String MESSAGE_DUPLICATE_INTERVIEW = "Interviews list contains duplicate interview(s)."; + public static final String MESSAGE_DUPLICATE_POSITION = "Positions list contains duplicate position(s)."; + + private final List applicants = new ArrayList<>(); + private final List interviews = new ArrayList<>(); + private final List positions = new ArrayList<>(); + + /** + * Constructs a {@code JsonSerializableHireLah} with the given persons. + */ + @JsonCreator + public JsonSerializableHireLah(@JsonProperty("applicants") List applicants, + @JsonProperty("interviews") List interviews, + @JsonProperty("positions") List positions) { + this.applicants.addAll(applicants); + this.interviews.addAll(interviews); + this.positions.addAll(positions); + } + + /** + * Converts a given {@code ReadOnlyHireLah} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableHireLah}. + */ + public JsonSerializableHireLah(ReadOnlyHireLah source) { + applicants.addAll(source.getApplicantList().stream().map(JsonAdaptedApplicant::new) + .collect(Collectors.toList())); + interviews.addAll(source.getInterviewList().stream().map(JsonAdaptedInterview::new) + .collect(Collectors.toList())); + positions.addAll(source.getPositionList().stream().map(JsonAdaptedPosition::new) + .collect(Collectors.toList())); + } + + /** + * Converts this address book into the model's {@code HireLah} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public HireLah toModelType() throws IllegalValueException { + HireLah hireLah = new HireLah(); + for (JsonAdaptedApplicant jsonAdaptedApplicant : applicants) { + Applicant applicant = jsonAdaptedApplicant.toModelType(); + if (hireLah.hasApplicant(applicant)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_APPLICANT); + } + hireLah.addApplicant(applicant); + } + + for (JsonAdaptedPosition jsonAdaptedPosition : positions) { + Position position = jsonAdaptedPosition.toModelType(); + if (hireLah.hasPosition(position)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_POSITION); + } + hireLah.addPosition(position); + } + + for (JsonAdaptedInterview jsonAdaptedInterview : interviews) { + Interview interview = jsonAdaptedInterview.toModelType(); + Applicant interviewApplicant = interview.getApplicant(); + Position interviewPosition = interview.getPosition(); + + if (hireLah.hasApplicant(interviewApplicant)) { + interview.setApplicant(hireLah.getApplicantUsingStorage(interviewApplicant)); + } + + if (hireLah.hasPosition(interviewPosition)) { + interview.setPosition(hireLah.getPositionUsingStorage(interviewPosition)); + } + + if (hireLah.hasInterview(interview)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_INTERVIEW); + } + hireLah.addInterview(interview); + } + + return hireLah; + } + +} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index beda8bd9f11..6b3ec1e859b 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -5,14 +5,14 @@ import java.util.Optional; import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyHireLah; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends HireLahStorage, UserPrefsStorage { @Override Optional readUserPrefs() throws DataConversionException, IOException; @@ -21,12 +21,12 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; @Override - Path getAddressBookFilePath(); + Path getHireLahFilePath(); @Override - Optional readAddressBook() throws DataConversionException, IOException; + Optional readHireLah() throws DataConversionException, IOException; @Override - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + void saveHireLah(ReadOnlyHireLah hireLah) throws IOException; } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index 6cfa0162164..ccc21ba53d2 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -7,24 +7,24 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyHireLah; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; /** - * Manages storage of AddressBook data in local storage. + * Manages storage of HireLah data in local storage. */ public class StorageManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); - private AddressBookStorage addressBookStorage; + private HireLahStorage hireLahStorage; private UserPrefsStorage userPrefsStorage; /** * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}. */ - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { - this.addressBookStorage = addressBookStorage; + public StorageManager(HireLahStorage hireLahStorage, UserPrefsStorage userPrefsStorage) { + this.hireLahStorage = hireLahStorage; this.userPrefsStorage = userPrefsStorage; } @@ -46,33 +46,33 @@ public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { } - // ================ AddressBook methods ============================== + // ================ HireLah methods ============================== @Override - public Path getAddressBookFilePath() { - return addressBookStorage.getAddressBookFilePath(); + public Path getHireLahFilePath() { + return hireLahStorage.getHireLahFilePath(); } @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(addressBookStorage.getAddressBookFilePath()); + public Optional readHireLah() throws DataConversionException, IOException { + return readHireLah(hireLahStorage.getHireLahFilePath()); } @Override - public Optional readAddressBook(Path filePath) throws DataConversionException, IOException { - logger.fine("Attempting to read data from file: " + filePath); - return addressBookStorage.readAddressBook(filePath); + public Optional readHireLah(Path filePath) throws DataConversionException, IOException { + logger.info("Attempting to read data from file: " + filePath); + return hireLahStorage.readHireLah(filePath); } @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath()); + public void saveHireLah(ReadOnlyHireLah hireLah) throws IOException { + saveHireLah(hireLah, hireLahStorage.getHireLahFilePath()); } @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { + public void saveHireLah(ReadOnlyHireLah hireLah, Path filePath) throws IOException { logger.fine("Attempting to write to data file: " + filePath); - addressBookStorage.saveAddressBook(addressBook, filePath); + hireLahStorage.saveHireLah(hireLah, filePath); } } diff --git a/src/main/java/seedu/address/ui/ApplicantCard.java b/src/main/java/seedu/address/ui/ApplicantCard.java new file mode 100644 index 00000000000..2844b0a3033 --- /dev/null +++ b/src/main/java/seedu/address/ui/ApplicantCard.java @@ -0,0 +1,93 @@ +package seedu.address.ui; + +import java.util.Comparator; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.applicant.Applicant; + +/** + * An UI component that displays information of a {@code Applicant}. + */ +public class ApplicantCard extends UiPart { + + private static final String FXML = "ApplicantListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on HireLah level 4 + */ + + public final Applicant applicant; + + @FXML + private HBox applicantCardPane; + @FXML + private Label name; + @FXML + private Label id; + @FXML + private Label phone; + @FXML + private Label address; + @FXML + private Label email; + @FXML + private Label gender; + @FXML + private Label age; + @FXML + private FlowPane tags; + @FXML + private Label applicantstatus; + + /** + * Creates a {@code ApplicantCard} with the given {@code Applicant} and index to display. + */ + public ApplicantCard(Applicant applicant, int displayedIndex) { + super(FXML); + this.applicant = applicant; + id.setText(displayedIndex + ". "); + name.setText(applicant.getName().fullName); + phone.setText(applicant.getPhone().value); + address.setText(applicant.getAddress().value); + email.setText(applicant.getEmail().value); + gender.setText(applicant.getGender().value.equals("M") ? "Male" : "Female"); + age.setText(applicant.getAge().value + " years old"); + applicant.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + + String status = applicant.getStatus().toString(); + applicantstatus.setText(status); + if (status.equals("Available")) { + applicantstatus.setStyle("-fx-background-color: #247a32;"); + } else { + applicantstatus.setStyle("-fx-background-color: #9f8331;"); + } + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ApplicantCard)) { + return false; + } + + // state check + ApplicantCard card = (ApplicantCard) other; + return id.getText().equals(card.id.getText()) + && applicant.equals(card.applicant); + } +} diff --git a/src/main/java/seedu/address/ui/ApplicantListPanel.java b/src/main/java/seedu/address/ui/ApplicantListPanel.java new file mode 100644 index 00000000000..a8ec5d04093 --- /dev/null +++ b/src/main/java/seedu/address/ui/ApplicantListPanel.java @@ -0,0 +1,48 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.applicant.Applicant; + +/** + * Panel containing the list of applicants. + */ +public class ApplicantListPanel extends UiPart { + private static final String FXML = "ApplicantListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ApplicantListPanel.class); + + @FXML + private ListView applicantListView; + + /** + * Creates a {@code ApplicantListPanel} with the given {@code ObservableList}. + */ + public ApplicantListPanel(ObservableList applicantList) { + super(FXML); + applicantListView.setItems(applicantList); + applicantListView.setCellFactory(listView -> new ApplicantListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Applicant} using a {@code ApplicantCard}. + */ + class ApplicantListViewCell extends ListCell { + @Override + protected void updateItem(Applicant applicant, boolean empty) { + super.updateItem(applicant, empty); + + if (empty || applicant == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ApplicantCard(applicant, getIndex() + 1).getRoot()); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 9e75478664b..09f660f15ec 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -1,9 +1,15 @@ package seedu.address.ui; +import java.io.FileNotFoundException; +import java.util.ArrayList; + import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; +import seedu.address.commons.exceptions.ExportCsvOpenException; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; @@ -17,6 +23,8 @@ public class CommandBox extends UiPart { private static final String FXML = "CommandBox.fxml"; private final CommandExecutor commandExecutor; + private ArrayList previousCommands; + private int commandIndex; @FXML private TextField commandTextField; @@ -29,6 +37,31 @@ public CommandBox(CommandExecutor commandExecutor) { this.commandExecutor = commandExecutor; // calls #setStyleToDefault() whenever there is a change to the text of the command box. commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); + commandTextField.setOnKeyPressed(this::handleKeyPressed); + + previousCommands = new ArrayList<>(); + commandIndex = -1; + } + + /** + * Handles the event when a key is pressed. + */ + private void handleKeyPressed(KeyEvent keyEvent) { + if (keyEvent.getCode() == KeyCode.UP && commandIndex >= 0) { + commandTextField.setText(previousCommands.get(commandIndex)); + commandTextField.positionCaret(commandTextField.getText().length()); + if (commandIndex > 0) { + commandIndex--; + } + } else if (keyEvent.getCode() == KeyCode.DOWN && commandIndex < previousCommands.size()) { + if (commandIndex == previousCommands.size() - 1) { + commandTextField.setText(""); + } else { + commandIndex++; + commandTextField.setText(previousCommands.get(commandIndex)); + commandTextField.positionCaret(commandTextField.getText().length()); + } + } } /** @@ -37,6 +70,7 @@ public CommandBox(CommandExecutor commandExecutor) { @FXML private void handleCommandEntered() { String commandText = commandTextField.getText(); + if (commandText.equals("")) { return; } @@ -44,8 +78,13 @@ private void handleCommandEntered() { try { commandExecutor.execute(commandText); commandTextField.setText(""); - } catch (CommandException | ParseException e) { + + previousCommands.add(commandText); + commandIndex = previousCommands.size() - 1; + } catch (CommandException | ParseException | ExportCsvOpenException e) { setStyleToIndicateCommandFailure(); + } catch (FileNotFoundException e) { + e.printStackTrace(); } } @@ -79,7 +118,8 @@ public interface CommandExecutor { * * @see seedu.address.logic.Logic#execute(String) */ - CommandResult execute(String commandText) throws CommandException, ParseException; + CommandResult execute(String commandText) throws CommandException, ParseException, FileNotFoundException, + ExportCsvOpenException; } } diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 9a665915949..103a33e0cdd 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,8 +15,9 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; - public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; + public static final String USERGUIDE_URL = "https://ay2122s2-cs2103-w17-4.github.io/tp/UserGuide.html"; + private static final String USERGUIDE_MESSAGE = + "Refer to the HireLah user guide for more information:\n" + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); private static final String FXML = "HelpWindow.fxml"; @@ -27,21 +28,29 @@ public class HelpWindow extends UiPart { @FXML private Label helpMessage; + @FXML + private Label ugMessage; + + private String helpDescription; + + /** * Creates a new HelpWindow. * * @param root Stage to use as the root of the HelpWindow. */ - public HelpWindow(Stage root) { + public HelpWindow(Stage root, String helpDescription) { super(FXML, root); - helpMessage.setText(HELP_MESSAGE); + this.helpDescription = helpDescription; + helpMessage.setText(this.helpDescription); + ugMessage.setText(USERGUIDE_MESSAGE); } /** * Creates a new HelpWindow. */ - public HelpWindow() { - this(new Stage()); + public HelpWindow(String helpDescription) { + this(new Stage(), helpDescription); } /** @@ -99,4 +108,9 @@ private void copyUrl() { url.putString(USERGUIDE_URL); clipboard.setContent(url); } + + public void setHelpDescription(String helpDescription) { + this.helpDescription = helpDescription; + helpMessage.setText(this.helpDescription); + } } diff --git a/src/main/java/seedu/address/ui/InterviewCard.java b/src/main/java/seedu/address/ui/InterviewCard.java new file mode 100644 index 00000000000..2aec2e56376 --- /dev/null +++ b/src/main/java/seedu/address/ui/InterviewCard.java @@ -0,0 +1,93 @@ +package seedu.address.ui; + +import java.time.format.DateTimeFormatter; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.applicant.Applicant; +import seedu.address.model.interview.Interview; + +/** + * An UI component that displays information of a {@code Interview}. + */ +public class InterviewCard extends UiPart { + + private static final String FXML = "InterviewListCard.fxml"; + private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE dd MMM yyyy, h:mma"); + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on HireLah level 4 + */ + + public final Interview interview; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label date; + @FXML + private Label role; + @FXML + private Label name; + @FXML + private Label interviewphone; + @FXML + private Label interviewemail; + @FXML + private Label interviewstatus; + + /** + * Creates a {@code InterviewCard} with the given {@code Interview} and index to display. + */ + public InterviewCard(Interview interview, int displayedIndex) { + super(FXML); + this.interview = interview; + + id.setText(displayedIndex + ". "); + date.setText(interview.getDate().format(formatter)); + role.setText(interview.getPosition().getPositionName().positionName); + + Applicant applicant = interview.getApplicant(); + name.setText(applicant.getName().fullName); + interviewphone.setText(applicant.getPhone().value); + interviewemail.setText(applicant.getEmail().value); + + String status = interview.getStatus().toString(); + interviewstatus.setText(status); + if (status.equals("Pending")) { + interviewstatus.setStyle("-fx-background-color: #9f8331;"); + } else if (status.equals("Passed - Waiting for Applicant")) { + interviewstatus.setStyle("-fx-background-color: #4187a1;"); + } else if (status.equals("Failed") || status.equals("Rejected")) { + interviewstatus.setStyle("-fx-background-color: #b92c2c;"); + } else if (status.equals("Accepted")) { + interviewstatus.setStyle("-fx-background-color: #319f43;"); + } + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof InterviewCard)) { + return false; + } + + // state check + InterviewCard card = (InterviewCard) other; + return id.getText().equals(card.id.getText()) + && interview.equals(card.interview); + } +} diff --git a/src/main/java/seedu/address/ui/InterviewListPanel.java b/src/main/java/seedu/address/ui/InterviewListPanel.java new file mode 100644 index 00000000000..7fed386366f --- /dev/null +++ b/src/main/java/seedu/address/ui/InterviewListPanel.java @@ -0,0 +1,49 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.interview.Interview; + + +/** + * Panel containing the list of interviews. + */ +public class InterviewListPanel extends UiPart { + private static final String FXML = "InterviewListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(PositionListPanel.class); + + @FXML + private ListView interviewListView; + + /** + * Creates a {@code ApplicantListPanel} with the given {@code ObservableList}. + */ + public InterviewListPanel(ObservableList interviewList) { + super(FXML); + interviewListView.setItems(interviewList); + interviewListView.setCellFactory(listView -> new InterviewListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Position} using a {@code PositionCard}. + */ + class InterviewListViewCell extends ListCell { + @Override + protected void updateItem(Interview interview, boolean empty) { + super.updateItem(interview, empty); + + if (empty || interview == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new InterviewCard(interview, getIndex() + 1).getRoot()); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 9106c3aa6e5..0310e813200 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -1,17 +1,24 @@ package seedu.address.ui; +import java.io.FileNotFoundException; import java.util.logging.Logger; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.MenuItem; +import javafx.scene.control.SingleSelectionModel; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; import javafx.stage.Stage; +import seedu.address.commons.core.DataType; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.ExportCsvOpenException; +import seedu.address.logic.HelpArgument; import seedu.address.logic.Logic; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; @@ -31,7 +38,9 @@ public class MainWindow extends UiPart { private Logic logic; // Independent Ui parts residing in this Ui container - private PersonListPanel personListPanel; + private ApplicantListPanel applicantListPanel; + private PositionListPanel positionListPanel; + private InterviewListPanel interviewListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @@ -42,7 +51,13 @@ public class MainWindow extends UiPart { private MenuItem helpMenuItem; @FXML - private StackPane personListPanelPlaceholder; + private StackPane applicantListPanelPlaceholder; + + @FXML + private StackPane positionListPanelPlaceholder; + + @FXML + private StackPane interviewListPanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; @@ -50,6 +65,9 @@ public class MainWindow extends UiPart { @FXML private StackPane statusbarPlaceholder; + @FXML + private TabPane dataTypeTabs; + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ @@ -65,7 +83,7 @@ public MainWindow(Stage primaryStage, Logic logic) { setAccelerators(); - helpWindow = new HelpWindow(); + helpWindow = new HelpWindow(HelpArgument.OVERALL_HELPING_DESCRIPTION); } public Stage getPrimaryStage() { @@ -110,13 +128,19 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + applicantListPanel = new ApplicantListPanel(logic.getFilteredApplicantList()); + applicantListPanelPlaceholder.getChildren().add(applicantListPanel.getRoot()); + + positionListPanel = new PositionListPanel(logic.getFilteredPositionList()); + positionListPanelPlaceholder.getChildren().add(positionListPanel.getRoot()); + + interviewListPanel = new InterviewListPanel(logic.getFilteredInterviewList()); + interviewListPanelPlaceholder.getChildren().add(interviewListPanel.getRoot()); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); + StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getHireLahFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); CommandBox commandBox = new CommandBox(this::executeCommand); @@ -136,10 +160,9 @@ private void setWindowDefaultSize(GuiSettings guiSettings) { } /** - * Opens the help window or focuses on it if it's already opened. + * Handle help command based on command */ - @FXML - public void handleHelp() { + private void handleHelp() { if (!helpWindow.isShowing()) { helpWindow.show(); } else { @@ -147,6 +170,15 @@ public void handleHelp() { } } + /** + * Opens the help window or focuses on it if it's already opened. + */ + @FXML + public void handleHelpTab() { + helpWindow.setHelpDescription(HelpArgument.OVERALL_HELPING_DESCRIPTION); + handleHelp(); + } + void show() { primaryStage.show(); } @@ -163,8 +195,8 @@ private void handleExit() { primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; + public ApplicantListPanel getApplicantListPanel() { + return applicantListPanel; } /** @@ -172,13 +204,16 @@ public PersonListPanel getPersonListPanel() { * * @see seedu.address.logic.Logic#execute(String) */ - private CommandResult executeCommand(String commandText) throws CommandException, ParseException { + private CommandResult executeCommand(String commandText) throws CommandException, ParseException, + FileNotFoundException, ExportCsvOpenException { try { CommandResult commandResult = logic.execute(commandText); logger.info("Result: " + commandResult.getFeedbackToUser()); resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); if (commandResult.isShowHelp()) { + resultDisplay.setFeedbackToUser("Display help"); + helpWindow.setHelpDescription(commandResult.getFeedbackToUser()); handleHelp(); } @@ -186,11 +221,23 @@ private CommandResult executeCommand(String commandText) throws CommandException handleExit(); } + SingleSelectionModel selectionModel = dataTypeTabs.getSelectionModel(); + if (commandResult.getDataType() == DataType.APPLICANT) { + selectionModel.select(0); + } else if (commandResult.getDataType() == DataType.POSITION) { + selectionModel.select(1); + } else if (commandResult.getDataType() == DataType.INTERVIEW) { + selectionModel.select(2); + } + return commandResult; - } catch (CommandException | ParseException e) { + } catch (CommandException | ParseException | ExportCsvOpenException e) { logger.info("Invalid command: " + commandText); resultDisplay.setFeedbackToUser(e.getMessage()); throw e; + } catch (FileNotFoundException e) { + e.printStackTrace(); + throw e; } } } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java deleted file mode 100644 index 7fc927bc5d9..00000000000 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ /dev/null @@ -1,77 +0,0 @@ -package seedu.address.ui; - -import java.util.Comparator; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.layout.FlowPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import seedu.address.model.person.Person; - -/** - * An UI component that displays information of a {@code Person}. - */ -public class PersonCard extends UiPart { - - private static final String FXML = "PersonListCard.fxml"; - - /** - * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. - * As a consequence, UI elements' variable names cannot be set to such keywords - * or an exception will be thrown by JavaFX during runtime. - * - * @see The issue on AddressBook level 4 - */ - - public final Person person; - - @FXML - private HBox cardPane; - @FXML - private Label name; - @FXML - private Label id; - @FXML - private Label phone; - @FXML - private Label address; - @FXML - private Label email; - @FXML - private FlowPane tags; - - /** - * Creates a {@code PersonCode} with the given {@code Person} and index to display. - */ - public PersonCard(Person person, int displayedIndex) { - super(FXML); - this.person = person; - id.setText(displayedIndex + ". "); - name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - person.getTags().stream() - .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof PersonCard)) { - return false; - } - - // state check - PersonCard card = (PersonCard) other; - return id.getText().equals(card.id.getText()) - && person.equals(card.person); - } -} diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java deleted file mode 100644 index f4c501a897b..00000000000 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ /dev/null @@ -1,49 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.layout.Region; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; - -/** - * Panel containing the list of persons. - */ -public class PersonListPanel extends UiPart { - private static final String FXML = "PersonListPanel.fxml"; - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - - @FXML - private ListView personListView; - - /** - * Creates a {@code PersonListPanel} with the given {@code ObservableList}. - */ - public PersonListPanel(ObservableList personList) { - super(FXML); - personListView.setItems(personList); - personListView.setCellFactory(listView -> new PersonListViewCell()); - } - - /** - * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. - */ - class PersonListViewCell extends ListCell { - @Override - protected void updateItem(Person person, boolean empty) { - super.updateItem(person, empty); - - if (empty || person == null) { - setGraphic(null); - setText(null); - } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); - } - } - } - -} diff --git a/src/main/java/seedu/address/ui/PositionCard.java b/src/main/java/seedu/address/ui/PositionCard.java new file mode 100644 index 00000000000..76a9d12fefb --- /dev/null +++ b/src/main/java/seedu/address/ui/PositionCard.java @@ -0,0 +1,85 @@ +package seedu.address.ui; + +import java.util.Comparator; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.position.Position; + +/** + * An UI component that displays information of a {@code Position}. + */ +public class PositionCard extends UiPart { + + private static final String FXML = "PositionListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on HireLah level 4 + */ + + public final Position position; + + @FXML + private HBox positionCardPane; + @FXML + private Label name; + @FXML + private Label id; + @FXML + private Label description; + @FXML + private Label openings; + @FXML + private Label offered; + @FXML + private FlowPane requirements; + + /** + * Creates a {@code PositionCard} with the given {@code Position} and index to display. + */ + public PositionCard(Position position, int displayedIndex) { + super(FXML); + this.position = position; + + id.setText(displayedIndex + ". "); + name.setText(position.getPositionName().positionName); + description.setText(position.getDescription().descriptionText); + + int numofOpenings = position.getPositionOpenings().getCount(); + openings.setText(numofOpenings + (numofOpenings <= 1 ? " opening" : " openings")); + if (numofOpenings == 0) { + openings.setStyle("-fx-text-fill: #ffa4a4"); + } + + offered.setText(position.getPositionOffers().getCount() + " offered"); + + position.getRequirements().stream() + .sorted(Comparator.comparing(req -> req.requirementText)) + .forEach(req -> requirements.getChildren().add(new Label(req.requirementText))); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PositionCard)) { + return false; + } + + // state check + PositionCard card = (PositionCard) other; + return id.getText().equals(card.id.getText()) + && position.equals(card.position); + } +} diff --git a/src/main/java/seedu/address/ui/PositionListPanel.java b/src/main/java/seedu/address/ui/PositionListPanel.java new file mode 100644 index 00000000000..51f5cda3346 --- /dev/null +++ b/src/main/java/seedu/address/ui/PositionListPanel.java @@ -0,0 +1,50 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.position.Position; + + +/** + * Panel containing the list of positions. + */ +public class PositionListPanel extends UiPart { + private static final String FXML = "PositionListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(PositionListPanel.class); + + @FXML + private ListView positionListView; + + /** + * Creates a {@code PositionListPanel} with the given {@code ObservableList}. + */ + public PositionListPanel(ObservableList positionList) { + super(FXML); + + positionListView.setItems(positionList); + positionListView.setCellFactory(listView -> new PositionListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Position} using a {@code PositionCard}. + */ + class PositionListViewCell extends ListCell { + @Override + protected void updateItem(Position position, boolean empty) { + super.updateItem(position, empty); + + if (empty || position == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new PositionCard(position, getIndex() + 1).getRoot()); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..25dd54d6c4a 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/hirelah_icon.png"; private Logic logic; private MainWindow mainWindow; diff --git a/src/main/resources/images/hirelah_icon.png b/src/main/resources/images/hirelah_icon.png new file mode 100644 index 00000000000..f8232dda378 Binary files /dev/null and b/src/main/resources/images/hirelah_icon.png differ diff --git a/src/main/resources/view/ApplicantListCard.fxml b/src/main/resources/view/ApplicantListCard.fxml new file mode 100644 index 00000000000..5b82cb1d317 --- /dev/null +++ b/src/main/resources/view/ApplicantListCard.fxml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + +
+
+
diff --git a/src/main/resources/view/ApplicantListPanel.fxml b/src/main/resources/view/ApplicantListPanel.fxml new file mode 100644 index 00000000000..cc5c4af1d7b --- /dev/null +++ b/src/main/resources/view/ApplicantListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..786c71618f8 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,32 +1,36 @@ +* { + -fx-font-family: 'sans-serif'; +} + .background { - -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ + -fx-background-color: #2B2B2B; + background-color: #2B2B2B; /* Used in the default.html file */ } .label { -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; + -fx-font-family: "sans-serif"; -fx-text-fill: #555555; -fx-opacity: 0.9; } .label-bright { -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; + -fx-font-family: "sans-serif"; -fx-text-fill: white; -fx-opacity: 1; } .label-header { -fx-font-size: 32pt; - -fx-font-family: "Segoe UI Light"; + -fx-font-family: "sans-serif"; -fx-text-fill: white; -fx-opacity: 1; } .text-field { -fx-font-size: 12pt; - -fx-font-family: "Segoe UI Semibold"; + -fx-font-family: "sans-serif"; } .tab-pane { @@ -66,7 +70,7 @@ .table-view .column-header .label { -fx-font-size: 20pt; - -fx-font-family: "Segoe UI Light"; + -fx-font-family: "sans-serif"; -fx-text-fill: white; -fx-alignment: center-left; -fx-opacity: 1; @@ -97,18 +101,15 @@ -fx-label-padding: 0 0 0 0; -fx-graphic-text-gap : 0; -fx-padding: 0 0 0 0; + -fx-background-color: #222222; } -.list-cell:filled:even { - -fx-background-color: #3c3e3f; -} - -.list-cell:filled:odd { - -fx-background-color: #515658; +.list-cell:filled { + -fx-border-color: #2B2B2B transparent #2B2B2B transparent; } .list-cell:filled:selected { - -fx-background-color: #424d5f; + -fx-background-color: derive(#222222, 5%); } .list-cell:filled:selected #cardPane { @@ -121,35 +122,40 @@ } .cell_big_label { - -fx-font-family: "Segoe UI Semibold"; + -fx-font-family: "sans-serif"; -fx-font-size: 16px; -fx-text-fill: #010504; } +.cell_medium_label { + -fx-font-family: "sans-serif"; + -fx-font-size: 14px; + -fx-text-fill: #010504; +} + .cell_small_label { - -fx-font-family: "Segoe UI"; + -fx-font-family: "sans-serif"; -fx-font-size: 13px; -fx-text-fill: #010504; } .stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #2B2B2B; } .pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); + -fx-background-color: #2B2B2B; -fx-border-top-width: 1px; } .status-bar { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: #2B2B2B; } .result-display { - -fx-background-color: transparent; - -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; + -fx-background-color: #222222; + -fx-font-family: "sans-serif"; + -fx-font-size: 12pt; -fx-text-fill: white; } @@ -158,8 +164,10 @@ } .status-bar .label { - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-font-family: "sans-serif"; + -fx-text-fill: derive(#2B2B2B, 90%); + -fx-font-style: italic; + -fx-font-size: 11pt; -fx-padding: 4px; -fx-pref-height: 30px; } @@ -185,7 +193,7 @@ } .context-menu { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: derive(#2B2B2B, 20%); } .context-menu .label { @@ -193,12 +201,13 @@ } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #2B2B2B; + -fx-spacing: 0; } .menu-bar .label { - -fx-font-size: 14pt; - -fx-font-family: "Segoe UI Light"; + -fx-font-size: 12pt; + -fx-font-family: "sans-serif"; -fx-text-fill: white; -fx-opacity: 0.9; } @@ -218,7 +227,7 @@ -fx-border-width: 2; -fx-background-radius: 0; -fx-background-color: #1d1d1d; - -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; + -fx-font-family: "sans-serif", Helvetica, Arial, sans-serif; -fx-font-size: 11pt; -fx-text-fill: #d8d8d8; -fx-background-insets: 0 0 0 0, 0, 1, 2; @@ -281,12 +290,49 @@ -fx-text-fill: white; } +.tab-pane .tab-header-area .tab-header-background { + -fx-opacity: 0; + -fx-spacing: 0; +} + +.tab-pane { + -fx-tab-min-width: 95px; + -fx-tab-min-height: 25px; +} + +.tab { + -fx-background-color: #2B2B2B; + -fx-focus-color: transparent; + -fx-border-width: 0 1 0 1; + -fx-border-color: derive(#2B2B2B, 15%); +} + +.tab:selected { + -fx-background-color: derive(#2B2B2B, 15%); + -fx-border-width: 0 0 2 0; + -fx-border-color: #FAF85D ; +} + +.tab-pane:focused > .tab-header-area > .headers-region > .tab:selected .focus-indicator { + -fx-border-color: -fx-focus-color; +} + +.tab-label { + -fx-font-size: 12pt; + -fx-font-family: "sans-serif"; + -fx-text-fill: derive(#1d1d1d, 90%); +} + +.tab:selected .tab-label { + -fx-text-fill: white; +} + .scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #222222; } .scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: derive(#2B2B2B, 20%); -fx-background-insets: 3; } @@ -307,7 +353,7 @@ -fx-padding: 8 1 8 1; } -#cardPane { +.cardPane { -fx-background-color: transparent; -fx-border-width: 0; } @@ -318,13 +364,12 @@ } #commandTextField { - -fx-background-color: transparent #383838 transparent #383838; + -fx-background-color: #222222; -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-font-family: "sans-serif"; + -fx-font-size: 12pt; -fx-text-fill: white; } @@ -333,7 +378,7 @@ } #resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; + -fx-background-color: #222222; -fx-background-radius: 0; } @@ -350,3 +395,47 @@ -fx-background-radius: 2; -fx-font-size: 11; } + +#requirements { + -fx-hgap: 7; + -fx-vgap: 3; +} + +#requirements .label { + -fx-text-fill: white; + -fx-background-color: #3e914f; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +#openings, #offered { + -fx-text-fill: #d0d0d0; +} + +#role { + -fx-text-fill: white; + -fx-background-color: #9f8331; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 3; + -fx-font-size: 12; +} + +#interviewphone, #interviewemail { + -fx-text-fill: #d0d0d0; +} + +#interviewstatus, #applicantstatus { + -fx-text-fill: white; + -fx-background-color: #9f8331; + -fx-padding: 3 3 3 3; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-font-size: 12; +} + +#statusbarPlaceholder { + -fx-background-color: #2B2B2B; +} diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css index 17e8a8722cd..c08f8e16fd7 100644 --- a/src/main/resources/view/HelpWindow.css +++ b/src/main/resources/view/HelpWindow.css @@ -1,13 +1,17 @@ -#copyButton, #helpMessage { +* { + -fx-font-family: 'sans-serif'; +} + +#copyButton, #helpMessage, #ugMessage { -fx-text-fill: white; } #copyButton { - -fx-background-color: dimgray; + -fx-background-color: derive(#2B2B2B, 30%); } #copyButton:hover { - -fx-background-color: gray; + -fx-background-color: derive(#2B2B2B, 60%); } #copyButton:armed { @@ -15,5 +19,5 @@ } #helpMessageContainer { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #2B2B2B; } diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index 5dea0adef70..8ab3268522b 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -1,15 +1,14 @@ - - - - - - - - + + + + + + + - + @@ -19,26 +18,23 @@ - + + + + - - + +