diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000000..9f1b24daddb --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +trap fail ERR + +TYPE="$(basename $0)" +TYPE_UPPER="${TYPE^}" + +function fail() { + if [ -n "${filename:-}" ]; then + echo -e '\033[1;91m### '"$TYPE_UPPER"' script '"$filename"' failed!\033[0m' + else + echo -e '\033[1;91m### '"$TYPE_UPPER"' hooks failed for an unknown reason!\033[0m' + fi + exit 1 +} + +# assume we're in repo root + +if [[ -d .git/hooks/${TYPE}.d ]]; then + for script in .git/hooks/${TYPE}.d/*; do + if [[ -f "$script" && -x "$script" ]]; then + filename="$(basename "$script")" + echo -e '\033[1;96m### Running '"$TYPE"' script '"$filename"'\033[0m' + bash $script + echo -e '\033[1;96m### Finished running '"$TYPE"' script '"$filename"'\033[0m\n' + fi + done +fi diff --git a/.githooks/pre-commit.d/fix-fxml-versions b/.githooks/pre-commit.d/fix-fxml-versions new file mode 100755 index 00000000000..c1d31324ea0 --- /dev/null +++ b/.githooks/pre-commit.d/fix-fxml-versions @@ -0,0 +1,29 @@ +#!/bin/bash +set -euo pipefail +JAVAFX_VER=11 + +# only care about staged changes for files ending in .fxml +git diff --cached --name-only | { grep -E "src/main/.*\.fxml$" || test $? == 1; } | while read fn; do + # suppress only exit code 1; let set -e catch everything else + javafx_lines="$(git show ":$fn" | { grep -E -o 'http://javafx.com/javafx/[^"]+' || test $? == 1; })" + + # sanity check: only one line + javafx_num_lines=$(wc -l <<< "$javafx_lines") + if [[ $javafx_num_lines -ne 1 ]]; then + >&2 echo "error: staged file $fn doesn't have exactly 1 line with javafx xmlns url (got: $javafx_num_lines)" + exit 1 + fi + + javafx_cur_ver=$(grep -E -o "[0-9]+" <<< "$javafx_lines") + + if [[ $javafx_cur_ver -ne $JAVAFX_VER ]]; then + echo "staged file $fn has incorrect javafx ver $javafx_cur_ver; changing to ver $JAVAFX_VER in working tree and index" + # working tree, hopefully it breaks nothing + sed -i "s|http://javafx.com/javafx/$javafx_cur_ver|http://javafx.com/javafx/$JAVAFX_VER|" "$fn" || test $? == 1 + + # index (i.e. staged) + partial_patch="$(git show ":$fn" | sed "s|http://javafx.com/javafx/$javafx_cur_ver|http://javafx.com/javafx/$JAVAFX_VER|" | { diff -u <(git show ":$fn") - || test $? == 1; } | tail -n +3)" + patch="--- a/$fn"$'\n'"+++ b/$fn"$'\n'"$partial_patch" + git apply --cached <<< "$patch" + fi +done diff --git a/.githooks/pre-commit.d/gradle-checks b/.githooks/pre-commit.d/gradle-checks new file mode 100755 index 00000000000..cfc12d0ced1 --- /dev/null +++ b/.githooks/pre-commit.d/gradle-checks @@ -0,0 +1,20 @@ +#!/bin/bash +set -euo pipefail + +# https://stackoverflow.com/a/3466183/1675299 + +unameOut="$(uname -s)" +case "${unameOut}" in + Linux*) machine=Linux;; + Darwin*) machine=Mac;; + CYGWIN*) machine=Cygwin;; + MINGW*) machine=MinGw;; + *) machine="UNKNOWN:${unameOut}" +esac + +if [[ "$machine" == "Cygwin" || "$machine" == "MinGw" ]]; then + ./gradlew.bat check +else + # assume *nix, I guess + ./gradlew check +fi diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 07acb40a13d..b769b27e7a4 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -38,7 +38,7 @@ jobs: - name: Build and check with Gradle run: ./gradlew check coverage - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v2 if: runner.os == 'Linux' with: file: ${{ github.workspace }}/build/reports/jacoco/coverage/coverage.xml diff --git a/.gitignore b/.gitignore index 71c9194e8bd..cbde7f9d4de 100644 --- a/.gitignore +++ b/.gitignore @@ -3,16 +3,21 @@ /build/ src/main/resources/docs/ -# IDEA files +# IDEA and other IDE files /.idea/ /out/ /*.iml +/.vscode/ + +# Misc build files +/bin/ # Storage/log files -/data/ +/data/ Change the sample details /config.json /preferences.json /*.log.* +/data/ # Test sandbox files src/test/data/sandbox/ @@ -20,3 +25,4 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ +gradle/wrapper/gradle-wrapper.properties diff --git a/README.md b/README.md index 13f5c77403f..1185f29d93d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) - -![Ui](docs/images/Ui.png) - -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. +[![CI Status](https://github.com/AY2122S2-CS2103T-T11-4/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2122S2-CS2103T-T11-4/tp/actions/workflows/gradle.yml) +[![Codecov](https://codecov.io/gh/AY2122S2-CS2103T-T11-4/tp/branch/master/graph/badge.svg?token=Z0PQIQXY29)](https://codecov.io/gh/AY2122S2-CS2103T-T11-4/tp) + +![Ui](docs/images/Ui.gif) + +PeopleSoft is a CLI-based contractor payroll management app. It helps **companies which offer contractor services** with managing how much each contractor is paid. You can: +- manage contractors +- manage jobs +- calculate monthly salary + +It is written with the OOP paradigm in mind and has ~11 KLoC. +View the User Guide and Developer Guide on our **[Website](https://ay2122s2-cs2103t-t11-4.github.io/tp/)**. +## Credits +Inter Font from https://github.com/rsms/inter + +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..1ebb14cdd8d 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id 'jacoco' } -mainClassName = 'seedu.address.Main' +mainClassName = 'peoplesoft.Main' sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -20,11 +20,6 @@ checkstyle { toolVersion = '8.29' } -test { - useJUnitPlatform() - finalizedBy jacocoTestReport -} - task coverage(type: JacocoReport) { sourceDirectories.from files(sourceSets.main.allSource.srcDirs) classDirectories.from files(sourceSets.main.output) @@ -40,8 +35,13 @@ task coverage(type: JacocoReport) { } } +test { + useJUnitPlatform() + finalizedBy coverage +} + dependencies { - String jUnitVersion = '5.4.0' + String jUnitVersion = '5.8.2' String javaFxVersion = '11' implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' @@ -66,7 +66,11 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'peoplesoft.jar' +} + +run { + enableAssertions = true } defaultTasks 'clean', 'test' diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 4c001417aea..4b090c3715b 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -340,6 +340,20 @@ + + diff --git a/data.example/addressbook.json b/data.example/addressbook.json new file mode 100644 index 00000000000..441a50bf20f --- /dev/null +++ b/data.example/addressbook.json @@ -0,0 +1,62 @@ +{ + "persons" : [ { + "id" : "1", + "name" : "Nicole Tan", + "phone" : "99338558", + "email" : "nicole@stffhub.org", + "address" : "1 Tech Drive, S138572", + "rate" : { + "amount" : "30.000000", + "duration" : "PT1H" + }, + "tagged" : [ "Intern", "Aircon" ] + }, { + "id" : "2", + "name" : "Kavya Singh", + "phone" : "96736637", + "email" : "kavya@stffhub.org", + "address" : "2 Orchard Turn, S238801", + "rate" : { + "amount" : "40.000000", + "duration" : "PT1H" + }, + "tagged" : [ "Senior", "Electrician" ] + }, { + "id" : "3", + "name" : "Ethan Lee", + "phone" : "91031282", + "email" : "ethan@stffhub.org", + "address" : "10 Anson Road, S079903", + "rate" : { + "amount" : "20.000000", + "duration" : "PT1H" + }, + "tagged" : [ "Appliances" ] + }, { + "id" : "4", + "name" : "Irfan Ibrahim", + "phone" : "92492021", + "email" : "irfan@stffhub.org", + "address" : "Blk 47 Tampines Street 20, #17-35", + "rate" : { + "amount" : "48.000000", + "duration" : "PT1H" + }, + "tagged" : [ "Painting" ] + }, { + "id" : "5", + "name" : "Arjun Khatau", + "phone" : "80445044", + "email" : "arjun@stffhub.org", + "address" : "50 Collyer Quay, S049321", + "rate" : { + "amount" : "33.000000", + "duration" : "PT1H" + }, + "tagged" : [ "Contract", "Aircon" ] + } ], + "jobs" : [ ], + "employment" : { }, + "jobIdState" : 0, + "personIdState" : 5 +} diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..b51102149f7 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,41 @@ 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 -### John Doe +### Li Zhong Fu - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/zhongfu)] +[[portfolio](team/zhongfu.md)] -* Role: Project Advisor +### Hong Yi En, Ian -### Jane Doe + - +[[github](http://github.com/ian-from-dover)] +[[portfolio](team/ian-from-dover.md)] -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +### Wrik Karmakar -* Role: Team Lead -* Responsibilities: UI + -### Johnny Doe +[[github](http://github.com/thewrik)] [[portfolio](team/thewrik.md)] - +### Elliot Lim Zhi Yong -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] + -* Role: Developer -* Responsibilities: Data +[[github](http://github.com/spyobird)] +[[portfolio](team/spyobird.md)] -### Jean Doe - +### Elumalai Oviya Dharshini -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] + -* Role: Developer -* Responsibilities: Dev Ops + Threading +[[github](http://github.com/ovidharshini)] +[[portfolio](team/ovidharshini.md)] -### James Doe - - - -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] - -* Role: Developer -* Responsibilities: UI diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 46eae8ee565..641bc05e738 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,47 +2,61 @@ layout: page title: Developer Guide --- + +## Introduction + +PeopleSoft is a desktop app for **calculating the salary for shift-based contractors**, optimized for use via a **Command Line Interface (CLI)**. If you are a **HR manager** and you can type fast, PeopleSoft can get your payroll tasks done **much faster** than traditional GUI apps. + +**PeopleSoft helps to:** +* Simplify the management of data +* Reduce menial labour +* Reduce mistakes due to human error in calculation / accidental edits +* Help employees be assured that their hours and pay are registered correctly in the system + +-------------------------------------------------------------------------------------------------------------------- + * Table of Contents {:toc} -------------------------------------------------------------------------------------------------------------------- -## **Acknowledgements** +## Acknowledgements +* Project adapted from [addressbook-level3](https://se-education.org/addressbook-level3/DeveloperGuide.html#product-scope) +* Layout of user stories adapted from [TAB](https://ay2122s1-cs2103t-f13-3.github.io/tp/DeveloperGuide.html#user-stories) * {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} -------------------------------------------------------------------------------------------------------------------- -## **Setting up, getting started** +## Setting up, getting started Refer to the guide [_Setting up and getting started_](SettingUp.md). -------------------------------------------------------------------------------------------------------------------- -## **Design** +## Design
-: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/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.
### Architecture - -The ***Architecture Diagram*** given above explains the high-level design of the App. +*Figure 1. Architecture diagram of the high-level design of PeopleSoft* Given below is a quick overview of main components and how they interact with each other. **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, -* 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. +**`Main`** has two classes called [`Main`](https://github.com/AY2122S2-CS2103T-T11-4/tp/blob/master/src/main/java/peoplesoft/Main.java) and [`MainApp`](https://github.com/AY2122S2-CS2103T-T11-4/tp/blob/master/src/main/java/peoplesoft/MainApp.java). It is responsible for, +* At app launch: Initializing the components in the correct sequence, and connecting them up with each other. +* At shut down: Shutting down the components and invoking cleanup methods where necessary. [**`Commons`**](#common-classes) represents a collection of classes used by multiple other components. -The rest of the App consists of four components. +The rest of the application consists of four components. * [**`UI`**](#ui-component): The UI of the App. * [**`Logic`**](#logic-component): The command executor. @@ -52,108 +66,239 @@ 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`. - +*Figure 2. Sequence diagram showing component interactions when the user enters the command `persondelete 1`* + Each of the four main components (also shown in the diagram above), -* defines its *API* in an `interface` with the same name as the Component. -* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. +* defines its *API* in an `interface` with the same name as the component. +* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point). For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. +*Figure 3. Partial class diagram of the interaction of components* + 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 the [`Ui.java`](https://github.com/AY2122S2-CS2103T-T11-4/tp/blob/master/src/main/java/peoplesoft/ui/Ui.java) interface. ![Structure of the UI Component](images/UiClassDiagram.png) +*Figure 4. Class diagram of GUI* -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. +The UI consists of a `MainWindow` that is made up of parts e.g.`SideBar`, `CommandBox`, `ResultDisplay`, `OverviewPage`, 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-CS2103T-T11-4/tp/blob/master/src/main/java/peoplesoft/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2122S2-CS2103T-T11-4/tp/blob/master/src/main/resources/view/MainWindow.fxml) The `UI` component, * executes user commands using the `Logic` component. * listens for changes to `Model` data so that the UI can be updated with the modified data. * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +* depends on some classes in the `Model` component, as it displays the `Person` and `Job` 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) +The **API** of this component is specified in the [`Logic.java`](https://github.com/AY2122S2-CS2103T-T11-4/tp/blob/master/src/main/java/peoplesoft/logic/Logic.java) interface. Here's a (partial) class diagram of the `Logic` component: +*Figure 5. 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. 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. +2. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `JobAddCommand`) which is executed by the `LogicManager`. +3. The command can communicate with the `Model` when it is executed (e.g. to add a person). +4. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. ![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) -
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +*Figure 6. Sequence diagram of the interactions within the `Logic` component for the `execute("delete 1")` API call* + +
:information_source: Note: The lifeline for `JobDeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command: +*Figure 7. Class diagram of the Parser component, a subcomponent of the Logic component* + How the parsing works: -* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. -* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. +* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `JobAddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `JobAddCommand`) which the `AddressBookParser` returns back as a `Command` object. +* All `XYZCommandParser` classes (e.g., `JobAddCommandParser`, `JobDeleteCommandParser`, ...) 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) +The **API** of this component is specified in the [`Model.java`](https://github.com/AY2122S2-CS2103T-T11-4/tp/blob/master/src/main/java/peoplesoft/model/Model.java) interface. +*Figure 8. Class diagram of the `Model` component* The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). +* stores the address book data - i.e. all `Person` objects (which are contained in a `UniquePersonList` object), all `Job` objects (which are contained in a `UniqueJobList` object), and auxiliary classes `Employment`, `PaymentHandler` etc. +* allows for the automatic serialization and deserialization of `AddressBook` objects (and any component objects, e.g. `Person`, `Email`, etc) to and from JSON. * stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the currently 'selected' `Job` 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. +* contains an `Employment` class which represents the associations between `Person` and `Job` 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.
- - +A more detailed model of `Person` and `Job` is given below. Notice the association class `Employment` between `Person` and `Job`. -
+![Details of Person and Job](images/BetterModelClassDiagram.png) +*Figure 9. Class diagram of `Person` and `Job`* ### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +The **API** of this component is specified in the [`Storage.java`](https://github.com/AY2122S2-CS2103T-T11-4/tp/blob/master/src/main/java/peoplesoft/storage/Storage.java) +*Figure 10. Class diagram of the Storage component* + The `Storage` component, -* can save both address book data and user preference data in json format, and read them back into corresponding objects. +* can save both address book data and user preference data in JSON format, and read them back into corresponding objects. * inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). * depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) ### Common classes -Classes used by multiple components are in the `seedu.addressbook.commons` package. +Classes used by multiple components are in the `peoplesoft.commons` package. -------------------------------------------------------------------------------------------------------------------- -## **Implementation** +## Implementation This section describes some noteworthy details on how certain features are implemented. +### JSON serialization and deserialization + +The serialization and deserialization of model objects (e.g. `AddressBook`, `UniquePersonList`, `Person`, `Job`, `Tag`) is handled by custom serializers and deserializers, implemented as nested class within each model class. + +These serializers and deserializers are automatically used by Jackson during serialization and deserialization. + +The serializer and deserializer for each class determine how the objects are to be serialized and deserialized, including but not limited to: +* which fields are to be stored, +* how each field should be (de)serialized, e.g. by directly converting it to/from a JSON type, or by delegating it to Jackson (which will use the serializer/deserializer for the field type), and +* how the fields and current object are to be represented as (or parsed from) JSON values, e.g. objects, strings, numbers. + +This architecture has some advantages: +* The serdes implementations are kept together with the related classes; developers adding new model classes will not have to modify files in other packages. + * The previous implementation (with `JsonAdaptedPerson` etc.) requires that developers update the `JsonAdapted` classes belonging in the `Storage` component; this may not be immediately evident to developers. +* Developers adding new model classes can incorporate existing types (that already have corresponding serializers/deserializers) without needing to duplicate the serdes code, unlike with the previous implementation. +* Developers will also not need to (practically) duplicate classes, e.g. `Job` -> `JsonAdaptedJob` (with the `@JsonCreator` annotation), just so that Jackson has something to serialize from/deserialize to. + +However, it also has some drawbacks: +* It can be rather verbose, since each serializer/deserializer class contains a portion of boilerplate code +* Developers writing serializers/deserializers will need to have basic knowledge of JSON, e.g. the types that are available, the structure of JSON objects and arrays, etc +* Some knowledge of Jackson components (e.g. `JsonParser`, `JsonGenerator`, `ObjectNode`) is also required, as developers will need to use them to write values to/read values from the internal Jackson representation of a JSON value/object. + +### The `Find` command + +The `Find` command is an enhancement of the `Find` feature provided in AB3. +It is structured using an object of `PersonContainsKeywordPredicate`, adapted from `NameContainsKeywordPredicate`. It +has the following attributes: +* `static final String COMMAND_WORD` initialised to `'find'` +* `static final String MESSAGE_USAGE` initialised to the relevant message. +* `PersonContainsKeywordPredicate predicate` used to find `Person` objects that match with the given keyword. + +Applying this filter to the entire list means that only `Persons` that match **ALL** the keywords would be retained in +the filtered list. + +#### The `PersonContainsKeywordPredicate` class + +The **match** itself is defined as follows (within the `PersonContainsKeywordPredicate` class which implements the +`Predicate` interface): +If a `Person` contains **ALL** the keywords passed in the query, either in their `name` field or as equivalent to an +element in their `tags` set of `Tag` objects, then, passed as a parameter to `test()`, +it is a valid match. + +The implementation is achieved through using stream manipulations to iterate through each person object, +and for any such object, iterating through each keyword passed in the query. The keyword is then checked if it is +contained within the name or among the tags. + +One motivation behind using streams rather than iteration was that streams can be better optimized, given the need or +bandwith arising later. + +### The `JobList` interface and `UniqueJobList` class + +The `JobList` is an interface for the list of jobs, that implements the `Iterable` interface and supports minimal list +operations. + +The `UniqueJobList` class implements the `JobList` interface to that enforces uniqueness between its elements and does +not allow nulls. +Furthermore, it has the following attributes, to interact with `java.fx` effectively. +* `ObservableList internalList` +* `ObservableList internalUnmodifiableList` + +### The `Job` class + +The `Job` class is an abstraction for a job stored in PeopleSoft. A `Job` object is immutable and contains the +following attributes: +* `String jobId` - Jobs are referenced by this attribute. +* `String desc` - A user-readable description of this job. +* `Duration duration` - The duration that a job has been worked. Is used together with `rate` to calculate the + total job earnings. Uses `java.time.Duration`. +* `boolean hasPaid` - A boolean to denote if this job is completed. Used to calculate the salary of a + `Person`. +* `boolean isFinal` - A boolean to denote if this job has finalized payments. + +The use of immutability ensures that there are no unintended side effects of modifying a `Job`. +Whenever a `Job` needs to be modified (for example setting the value of `hasPaid`), a new immutable copy +of the `Job` with the desired changes is created to replace the old instance. Two `Job` objects are considered +the same job if they share the same `jobId`, which can be compared using `Job#isSameJob()`. + +### The `Job` and `Person` association - `Employment` + +In order to represent how a job may be assigned to a person (or a person may take on a job) in real life, an +association class `Employment` is used. The class handles the following responsibilities: +* Assigning a `Job` to a `Person`. +* Removing all necessary associations on the deletion/edit of a `Job` or `Person`. +* Filtering the `Job` objects that are mapped to a `Person`. + +In the current implementation, there is a many-to-many mapping of `Job` objects to `Person` objects. An +association can be created using `Employment#associate(job, person)`. The jobs are internally referenced by +`jobId`, while the persons are referenced by `personId`. Currently, the class `Employment` is written as a +singleton. This may be changed to be a field of `AddressBook` due to potential obstacles with the testing of +the serialization/deserialization of the class. + +#### Design considerations: + +**Aspect: How the relational mapping between Job and Person is stored:** + +* **Alternative 1 (current choice):** Saves the mapping of `Job` objects to `Person` objects in `Employment`. + * Pros: Guarantee of commutative association. + * Cons: Harder to implement. + +* **Alternative 2 (rejected):** Saves a map of the ids of related `Person` objects in `Job` objects, and a map of the ids of related `Job` objects in `Person` objects. + * Pros: Easier to implement. + * Cons: Mutual associations are not guaranteed. The possible extension of a one-to-many relationship between `Person` and `Tag` would be harder to implement. + +### \[Proposed\] Addition of pay multipliers to Job +![MultiplierTag0](images/MultiplierTagInheritanceDiagram.png) + +The proposed mechanism for adding pay multipliers to `Job` is facilitated by `MultiplierTag`. `MultiplierTag` extends `Tag` with a multiplier addition timeline history, stored internally as a `MultiplierHistory` Map between pay multiplier values and their time of addition. + +The inclusion of `MultiplierTag` in calculating pay `Job` objects is facilitated by `PaymentHandler#createPendingPayments` which calls on the modified operation `Job#calculatePay(Set)`. `Job#calculatePay(Set)` returns the appropriately scaled pay amount after accounting for every tag the passed `Person` parameter has. `Job#calculatePay()` is called by calls `Job#calculatePay()` based on optional `Tag` parameters. + +#### Design considerations: +* **Alternative 1 (current choice):** Saves a Map of previous multipliers and time of addition in Tag. + * Pros: Easy to implement. Pay breakdown can be useful in the implementation of other features. + * Cons: May have performance issues in terms of memory usage. + +* **Alternative 2:** Saves pay amount as a fixed value and updates it when Tag is edited. + * Pros: Will use less memory as only one value is being stored. + * Cons: Loss of useful pay breakdown information. + ### \[Proposed\] Undo/redo feature #### Proposed Implementation @@ -235,13 +380,30 @@ The following activity diagram summarizes what happens when a user executes a ne _{more aspects and alternatives to be added}_ ### \[Proposed\] Data archiving +A simple archival feature can be easily implemented, as all of the app data can be (and is currently) stored in a single file. -_{Explain here how the data archiving feature will be implemented}_ +As such, it should be trivial to add an `archive` command, which saves a copy of the database to a different filename. Auto-archival should also be possible, e.g. by saving a copy of the database on every X changes, or if the last archive was made more than Y hours ago. +The archived data files may not be as user-friendly though -- restoring data archives will require users to copy an archived copy of the database to the expected location, then restarting the application. + +We might hence want to implement a rudimentary interface with which users can browse through older archives -- this interface might show basic information about each archive, such as: + +- date of archive +- number of employees/jobs +- size of archive + +...as well as provide an easy way to load the archive into the app temporarily, which can be implemented as follows: + +1. save the current database somewhere +2. make a copy of the archive +3. set the storage filename to point to the archive copy +4. reload the application, if required + +If the user desires to return to the original database, then we can simply load the database saved in Step 1 and reload the application. -------------------------------------------------------------------------------------------------------------------- -## **Documentation, logging, testing, configuration, dev-ops** +## Documentation, logging, testing, configuration, dev-ops * [Documentation guide](Documentation.md) * [Testing guide](Testing.md) @@ -251,48 +413,77 @@ _{Explain here how the data archiving feature will be implemented}_ -------------------------------------------------------------------------------------------------------------------- -## **Appendix: Requirements** +## Appendix: Requirements ### Product scope **Target user profile**: - -* has a need to manage a significant number of contacts +HR Managers of companies offering contractor services +* have a need to manage a significant number of employees and jobs +* employee pay is calculated based on hours worked * prefer desktop apps over other types * can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +* prefer typing to mouse interactions +* are reasonably comfortable using CLI apps + +**Value proposition**: +* Simplify the management of data +* Reduce menial labour +* Reduce mistakes due to human error in calculation / accidental edits +* Helps employees be assured that their hours and pay are registered correctly in the system -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app ### User stories +For convenience, our user stories have been categorized with three broad labels: +1. [E] - Employee-related functions +2. [J] - Job-related functions +3. [N] - Neither of the above + +Note: multiple labels can be applied to a single user story. 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 | +| Label | Priority | As a …​ | I want to …​ | So that I can…​ | +|-------|----------|----------------|--------------------------------------------------------------------------------|-------------------------------------------------------------------------------| +| N | `* *` | new user | see usage instructions | refer to instructions when I forget how to use the App | +| N | `* *` | potential user | see the app populated with sample data | easily see how the app will look like when it is in use | +| N | `* *` | HR Manager | log into separate modes for HR-related functions and for job-related functions | easily access relevant data for the type of work I am doing at any given time | +| N | `* * *` | HR Manager | load and save data in human-readable data files | I can backup the data externally or access it in a different application | | +| N | `* * *` | HR Manager | exit the application | +| E | `* * *` | HR Manager | add a new employee | | +| E | `* * *` | HR Manager | add tags to employees | identify their roles | +| E | `* * *` | HR Manager | edit an employee's information | rectify mistakes or update their personal information if need be | +| E | `* * *` | HR Manager | delete an employee | | +| E | `* * *` | HR Manager | delete all employees | mass-remove entries that I no longer need | +| E | `* * *` | HR Manager | list all employees | | +| E | `* * *` | HR Manager | find an employee by name or tag | locate details of employees without having to go through the entire list | +| J | `* * *` | HR Manager | add a new job | | +| J | `* * *` | HR Manager | mark a job as having been paid for | | +| J | `* * *` | HR Manager | mark a job as having been completed | | +| J | `* * *` | HR Manager | update a job | | +| J | `* * *` | HR Manager | delete a job | | +| J | `* * *` | HR Manager | delete all jobs | mass-remove entries that I no longer need | +| J | `* * *` | HR Manager | find a job by description | locate details of jobs without having to go through the entire list | +| EJ | `* * *` | HR Manager | assign an employee to a job | | +| EJ | `* * *` | HR Manager | view the salary owed to a given employee | pay them | +| EJ | `* * *` | HR Manager | pay for a given type of job | | +| EJ | `* * *` | HR Manager | see which jobs each employee is working on | pay them accordingly | +| EJ | `* *` | HR Manager | edit pay multiplier factors (e.g. overtime, experience, emergency on-calls) | apply changes in payment policies across the organization | -*{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 `PeopleSoft` and the **Actor** is the `user`, unless specified otherwise) -**Use case: Delete a person** +**Use case: Delete an employee** **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 employees +2. PeopleSoft shows a list of employees +3. User requests to delete a specific employee in the list +4. PeopleSoft deletes the employee Use case ends. @@ -304,24 +495,48 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli * 3a. The given index is invalid. - * 3a1. AddressBook shows an error message. + * 3a1. PeopleSoft shows an error message. Use case resumes at step 2. -*{More to be added}* +**Use case: Update an employee's data** -### Non-Functional Requirements +**MSS** -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. +1. User requests to list employees +2. PeopleSoft shows a list of employees +3. User requests to edit a specific employee in the list with the updated information +4. PeopleSoft updates the employee to match user input -*{More to be added}* + Use case ends. + +**Extensions** + +* 2a. The list is empty. + + Use case ends. + +* 3a. The given index is invalid. + + * 3a1. PeopleSoft shows an error message. + + Use case resumes at step 2. + +### Non-Functional Requirements + +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. Should not rely on database-management systems to store data. +4. Should not require an installer; should be packaged into a single reasonably-sized (i.e. within 100MB) JAR file. +5. Should not be hosted on remote servers. +6. Should not make use of proprietary third-party frameworks, libraries and services. +7. Should have a responsive GUI. GUI should function well (i.e., should not cause any resolution-related inconveniences to the user) for standard screen resolutions and higher and for screen scales 100% and 125%. GUI should be usable - even if suboptimal - for resolutions 1280x720 and higher and for screen scales 150%. +8. 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. ### Glossary * **Mainstream OS**: Windows, Linux, Unix, OS-X -* **Private contact detail**: A contact detail that is not meant to be shared with others +* **Standard screen resolution**: 1920x1080 -------------------------------------------------------------------------------------------------------------------- @@ -329,7 +544,7 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli Given below are instructions to test the app manually. -
:information_source: **Note:** These instructions only provide a starting point for testers to work on; +
:information_source: Note: These instructions only provide a starting point for testers to work on; testers are expected to do more *exploratory* testing.
@@ -338,40 +553,55 @@ testers are expected to do more *exploratory* testing. 1. Initial launch - 1. Download the jar file and copy into an empty folder + 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 contacts. The window size may not be optimum. 1. Saving window preferences - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. + 1. Resize the window to an optimum size. Move the window to a different location. Close the window. - 1. Re-launch the app by double-clicking the jar file.
+ 1. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ +### Adding an employee + +Adding an employee while employees are being shown + +Prerequisites: List all employees using the `list` command. Multiple employees in the list. + +- Test case: `personadd n/Nicole Tan p/99338558 e/nicole@stffhub.org a/1 Tech Drive, S138572 r/37.50 t/Hardware t/Senior`
Expected: An employee with the corresponding details will be added to the end of the employee list. Details of the added employee will be shown in the status message. +- Test case: `personadd p/99338558 e/nicole@stffhub.org a/1 Tech Drive, S138572 r/37.50 t/Hardware t/Senior`
Expected: No employee is added. The expected format of the `personadd` command will be shown in the status message. +- Test case: `personadd n/Nicole p/9 e/nicole@stffhub.org a/1 Tech Drive, S138572 r/37.50 t/Hardware t/Senior`
+ Expected: No employee is added. The expected format of `Phone` will be shown in the status message. + +Other incorrect `personadd` commands to try: `personadd`, `personadd n/Nicole`, `...`
+Expected: Similar to previous. -### Deleting a person +### Deleting an employee -1. Deleting a person while all persons are being shown +1. Deleting an employee while all employees are being shown - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + 1. Prerequisites: List all employees using the `list` command. Multiple employees in the list. - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. + 1. Test case: `delete 1`
+ Expected: First employee is deleted from the list. Details of the deleted employee shown in the status message. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + 1. Test case: `delete 0`
+ Expected: No employee is deleted. Error details shown in the status message. Status bar remains the same. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. + 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. -1. _{ more test cases …​ }_ +2. Deleting an employee after a find command is executed -### Saving data + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. -1. Dealing with missing/corrupted data files + 2. Test case: `delete 1`
+ Expected: First contact is deleted from the observable list. Details of the deleted contact shown in the status message. Entering the `list` command shows that the employee that was originally in the first index is not deleted (unless the position of the employee was the same after the `find` command). - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 3. Test case: `delete 0`
+ Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. -1. _{ more test cases …​ }_ + 4. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 275445bd551..6fa6da4f6b8 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -11,7 +11,7 @@ title: Setting up and getting started ## Setting up the project in your computer -
:exclamation: **Caution:** +
:exclamation: Caution: Follow the steps in the following guide precisely. Things will not work out if you deviate in some steps.
@@ -23,7 +23,7 @@ If you plan to use Intellij IDEA (highly recommended): 1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
:exclamation: Note: Importing a Gradle project is slightly different from importing a normal Java project. 1. **Verify the setup**: - 1. Run the `seedu.address.Main` and try a few commands. + 1. Run the `peoplesoft.Main` and try a few commands. 1. [Run the tests](Testing.md) to ensure they all pass. -------------------------------------------------------------------------------------------------------------------- @@ -34,7 +34,7 @@ If you plan to use Intellij IDEA (highly recommended): If using IDEA, follow the guide [_[se-edu/guides] IDEA: Configuring the code style_](https://se-education.org/guides/tutorials/intellijCodeStyle.html) to set up IDEA's coding style to match ours. -
:bulb: **Tip:** +
:bulb: Tip: Optionally, you can follow the guide [_[se-edu/guides] Using Checkstyle_](https://se-education.org/guides/tutorials/checkstyle.html) to find how to use the CheckStyle within IDEA e.g., to report problems _as_ you write code.
@@ -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 [PeopleSoft's architecture](DeveloperGuide.md#architecture). 1. **Do the tutorials** These tutorials will help you get acquainted with the codebase. diff --git a/docs/Testing.md b/docs/Testing.md index 8a99e82438a..1c8b71a11c9 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -29,8 +29,8 @@ There are two ways to run tests. This project has three types of tests: 1. *Unit tests* targeting the lowest level methods/classes.
- e.g. `seedu.address.commons.StringUtilTest` + e.g. `peoplesoft.commons.StringUtilTest` 1. *Integration tests* that are checking the integration of multiple code units (those code units are assumed to be working).
- e.g. `seedu.address.storage.StorageManagerTest` + e.g. `peoplesoft.storage.StorageManagerTest` 1. Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together.
- e.g. `seedu.address.logic.LogicManagerTest` + e.g. `peoplesoft.logic.LogicManagerTest` diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 3716f3ca8a4..f73cc7cdd1c 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -2,191 +2,837 @@ layout: page title: User Guide --- +Welcome to PeopleSoft! -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. +PeopleSoft is a desktop application for **calculating the salary for shift-based contractors**, optimized for use via a **command-line interface (CLI)**. If you are a **HR manager** and you can type fast, PeopleSoft can get your payroll tasks done **much faster** than traditional graphical user interface (GUI) applications. -* Table of Contents -{:toc} +You can input your employees' data and the jobs that you want to keep track of. +Then, you can assign the employees to the jobs that they are working on. +After the job is completed, you can mark the job as paid, and PeopleSoft will calculate how much each employee is to be paid based on their hourly rates. +You can also generate a payslip in comma-separated values (CSV) format for you and your employees to refer to. + +PeopleSoft simulates a real life workflow: + * The company receives a new job. + * The HR manager `add`s the job to PeopleSoft, and `assign`s employees to work on it. + * The employees start working. + * When the employees complete the job, the HR manager `mark`s the job as completed. + * Once it is time to pay the employees, `pay` out the job and `export` the payslips for the employees. + +## How to use this guide + +If this is your first time using PeopleSoft, we recommend that you read the user guide in order. If you come across unfamiliar terms used in this user guide, their definitions may be found in the [glossary](#glossary). + +If you are searching for information about specific features, you might find the [command summary section](#command-summary) useful in providing a brief overview of all commands. Alternatively, you may choose to navigate to the relevant sections of the user guide from the [table of contents](#toc) for more detailed explanations of features. +| Syntax | Purpose | +|------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------| +| `words in monospace font` | Commands to be typed into PeopleSoft | +|
:information_source: Boxes with a blue background and the :information_source: icon
| Contain relevant tips for using PeopleSoft | +|
:warning: Boxes with a yellow background and the :warning: icon
| Contain important warnings | + +-------------------------------------------------------------------------------------------------------------------- + +* Table of Contents +{:toc} -------------------------------------------------------------------------------------------------------------------- ## Quick start -1. Ensure you have Java `11` or above installed in your Computer. +### Running PeopleSoft for the first time + +1. Ensure you have Java 11 or above installed on your computer. Follow [**this guide**](https://docs.oracle.com/en/java/javase/11/install/overview-jdk-installation.html#GUID-8677A77F-231A-40F7-98B9-1FD0B48C346A) to do so. + +2. **Download** the latest version of `peoplesoft.jar` from [**here**](https://github.com/AY2122S2-CS2103T-T11-4/tp/releases). + +3. Place the `peoplesoft.jar` file anywhere on your computer (preferably within a new folder, as the configuration and data files will be stored in the same location). + +4. **Double-click** the file to start PeopleSoft. A window (similar to the one below) should appear shortly. + +
+ +**:information_source: Note:** If double-clicking the file does not start PeopleSoft, then open the command line interpreter for your system (e.g. Command Prompt or Windows Terminal on Windows, or Terminal on macOS and Linux), navigate to directory where the file is located (e.g. with `cd path/to/folder`), and run `java -jar peoplesoft.jar`. + +
+ +![Ui](images/screenshots/Ui_label.png)
+ +
+ The PeopleSoft interface +
+ +When PeopleSoft is started for the first time, it will be populated with sample data. You may delete this data with the [`clear`](#clear-clear-all-peoplesoft-data) command. + +A `data` folder and some configuration files (with a `.json` extension) will also be created in the folder that you run `peoplesoft.jar` from; this is where PeopleSoft data will be stored. + +[//]: # (Comment: trying this out, will remove if it doesnt work on webpage after I merge) +[Return to the Table of Contents](#toc) + +### Processing your first payment using the sample data + +The sample data is meant to help users get started with PeopleSoft. This is a tutorial of some basic features of PeopleSoft using the provided sample data. As such, it is not a comprehensive overview of every feature in PeopleSoft. You can refer to the [features](#features) section for more information about specific features. + +To start off, notice that the sample data contains some employees under the list of employees. Here, you can see the details of the employees, including their name, base pay and tags. + +Since the sample data does not include any jobs, we will need to create new ones. + +#### Create a job + +When your company receives a new job, you can add it to PeopleSoft. To create a job, you can use the `add` command. You will have to specify a name and duration using the `n/` and `d/` prefixes respectively. For this tutorial, we will create a two-hour-long aircon repair job. + +1. Type `add n/Repair aircon d/2` in the command box. + + This command will add a new job with the name "Repair aircon" and a duration of two hours. + +
+ + **:information_source: Note:** The attributes prefixed by `n/` and `d/` can be in any order. + +
+ +2. Hit **Enter** to run the command. + + ![Added repair Aircon](images/screenshots/tut_add.png) + +
+ A job with the name "Repair aircon" and a duration of two hours is created in the job list +
+
+ +#### Assign employee to a job + +PeopleSoft allows you to indicate which employees are in-charge of a certain job with the `assign` command. The `assign` command requires 1 job *index*, and at least 1 employee *index* (each prefixed by `i/`). These indexes can be found under the `#` column in both lists. + +1. Type `assign 1 i/1` in the command box. + + The first number in the command (`1`) refers to the *index* of the job. In this case, it refers to the first job in the list of jobs, which is the aircon repair job created earlier. + + The prefix `i/` denotes the index of the employee to be assigned to the job. In this case, it references the first employee in the list of employees, which is "Nicole Tan". Additional employees can also be assigned to this job by adding additional `i/` attributes. + +2. Hit **Enter** to run the command. + + A message should appear, indicating that the job "Repair aircon" is assigned to "Nicole Tan". + + ![Assigned to nicole](images/screenshots/tut_asgn.png) + +
+ Nicole Tan is assigned to the "Repair aircon" job +
+
+ +
+ +**:information_source: Note:** Index-based commands depend on the ordering of the items displayed in their respective lists. The search and list commands for employees and jobs can cause this order to vary. + +
+ +
+ +#### Find employees who are qualified + +1. Type `personfind aircon` in the command box. + + This command searches for "aircon" in the names or tags of employees. Search terms are not case-sensitive. + +2. Hit **Enter** to run the command. + + There should be two employees listed: "Nicole Tan", and "Arjun Khatau". Notice that the list of employees now only shows employees with the "Aircon" tag. + + ![Find people with the "Aircon" tag](images/screenshots/tut_find.png) + +
+ Nicole and Arjun are have the "Aircon" tag +
+
+ +3. Type `assign 1 i/2` in the command box. + + This assigns the second employee in the list (Arjun Khatau) to the first job. Notice that the employee index (2) now refers to the second employee in the current list (Arjun Khatau), instead of in the original list (Kavya Singh). + +4. Hit **Enter** to run the command. + + A message should appear, indicating that the job "Repair aircon" is assigned to "Arjun Khatau". + +#### Complete a job and pay employees + +A key feature of PeopleSoft is tracking the state of job completion and whether its payment has been processed; this can be done with the `mark` and `pay` commands, respectively. + +1. Type `mark 1` in the command box. + + This command marks the first job as completed. + +2. Hit **Enter** to run the command. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). + The job "Repair aircon" should be marked as completed, and a checkmark ![tick](images/apple-tick-emoji.png) should appear under the *Done* column. -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +Marking a job as completed creates pending payments for the job. The amounts pending payment are reflected in the *Unpaid* column in the employees list. -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.
- ![Ui](images/Ui.png) +Now that the "Repair aircon" job has been marked as completed, "Nicole Tan" and "Arjun Khatau" should have non-zero values under the *Unpaid* column. -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try: +This value reflects the amount of money that is pending payment to the employees. It is calculated from the employee's base rate and the job's duration. - * **`list`** : Lists all contacts. +1. Type `pay 1 y/` in the command box. - * **`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. + The prefix `y/` is a safeguard against accidental misuse of this command. This command finalizes payments for the given job, and is **irreversible**. After the job is finalized, it cannot be further modified, so do make sure that you intend to run this command before running it. - * **`delete`**`3` : Deletes the 3rd contact shown in the current list. +2. Hit **Enter** to run the command. - * **`clear`** : Deletes all contacts. + The payments for the job is now finalized, indicating that the employees have been paid for the job. This is also reflected in the checkmark ![tick](images/apple-tick-emoji.png) under the *Paid* column. - * **`exit`** : Exits the app. + ![Mark and pay](images/screenshots/tut_markpay.png) -1. Refer to the [Features](#features) below for details of each command. +
+ The job list after the job is marked as done and payments are finalised +
+
+ +#### Export employee payslips + +PeopleSoft also allows users to export a payslip for each employee as a comma-separated values (CSV) spreadsheet. + +1. Type `export 1` in the command box. + + This command exports the payslip for the first employee (Nicole Tan) to a CSV spreadsheet. + +2. Hit **Enter** to run the command. + + The payslip beginning with the employee's name is now saved in the PeopleSoft `data` folder. + +![Location of the data folder](images/screenshots/data_folder.png) + +
+ Exported employee data can be found under the data folder +
+ +This concludes the tutorial on the basic usage of PeopleSoft. You can refer to the [features](#features) section for more information about specific features. To clear the sample data, run the `clear` command. + +[Return to the Table of Contents](#toc) -------------------------------------------------------------------------------------------------------------------- +## Command summary -## Features +A handy reference for more experienced users who just need to know the format of a command. + +| Command | Format | Examples | +|:------------------------------------------------------------|:------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------| +| [`personadd`](#personadd-add-an-employee) | `personadd n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS r/RATE [t/TAG]...​` | `personadd n/Nicole Tan p/99338558 e/nicole@stffhub.org a/1 Tech Drive, S138572 r/37.50 t/Hardware t/Senior` | +| [`personedit`](#personedit-edit-an-employees-information) | `personedit EMPLOYEE_INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [r/RATE] [t/TAG]...​` | `personedit 2 n/Nicole Lee t/OS` | +| [`persondelete`](#persondelete-delete-an-employee) | `persondelete EMPLOYEE_INDEX` | `persondelete 3` | +| [`personfind`](#personfind-find-employees-by-name-or-tag) | `personfind KEYWORD [KEYWORD]...​` | `personfind Nicole Hardware`, `personfind Aircon` | +| [`personlist`](#personlist-list-all-employees) | `personlist` | NA | +| [`export`](#export-export-jobs-done-by-an-employee) | `export EMPLOYEE_INDEX` | `export 2` | +| [`clear`](#clear-clear-all-peoplesoft-data) | `clear` | NA | +| [`add`](#add-add-a-job) | `add n/NAME d/DURATION` | `add n/Fix HDB Lock d/1` | +| [`find`](#find-find-jobs-by-name) | `find KEYWORD [KEYWORD]...` | `find Painting Senior`, `find Painting` | +| [`list`](#list-list-all-jobs) | `list` | NA | +| [`delete`](#delete-delete-a-job) | `delete JOB_INDEX` | `delete 3` | +| [`assign`](#assign-assign-a-job-to-an-employee) | `assign JOB_INDEX i/EMPLOYEE_INDEX [i/EMPLOYEE_INDEX]...​` | `assign 2 i/1` | +| [`mark`](#mark-mark-or-unmark-a-job-as-done) | `mark JOB_INDEX` | `mark 2` | +| [`pay`](#pay-finalize-payments-for-a-job) | `pay JOB_INDEX y/` | `pay 2 y/` | +| [`exit`](#exit-exit-peoplesoft) | `exit` | NA | +| [`help`](#help-show-help-page) | `help` | NA | + +![Command format](images/screenshots/command_format.png) + +
+ Parts of a command in PeopleSoft +
**:information_source: Notes about the command format:**
-* Words in `UPPER_CASE` are the parameters to be supplied by the user.
- e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. +* Words in `UPPERCASE` are the attributes to be replaced with values provided by you.
+ e.g. in `personadd n/NAME`, `NAME` can be replaced with an actual name like in `personadd n/John Doe`. + +* Attributes can be in any order.
+ e.g. if the command asks for `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also fine. + +* If an attribute is expected only once but is used multiple times, only the last occurrence of the attribute will be taken.
+ e.g. if you specify `n/Jake n/Jason`, only `n/Jason` will be taken. + +* For commands that do not need attributes (like `help`, `list`, `exit` and `clear`), anything typed after the command word will be ignored.
+ e.g. `help 123` will be interpreted as `help`. * 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 specified as `n/John Doe t/friend` or `n/John Doe`. + +* Items with ellipses (`...​`) after them indicate that the items can be repeated any number of times.
+ e.g. `[t/TAG]...​` can be interpreted as `[t/TAG]`, `[t/TAG] [t/TAG]`, and so on. + +
+ +### Parameter constraints + +A handy reference for the constraints on input parameters for commands. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterPrefixConstraints
Namen/ +

Contains only alphanumeric characters and spaces.

+

Should not be empty.

+
Phone numberp/ +

Contains only numbers.

+

Should be at least three digits long.

+
Emaile/ +

Should be of the format name@domain.com where:

+
    +
  1. name contains only alphanumeric characters and the following special characters: +_.-.
  2. +
  3. name should start and end with alphanumeric characters.
  4. +
  5. domain contains only alphanumeric characters and the following special characters: .-.
  6. +
  7. domain should be at least two characters long.
  8. +
  9. domain should start and end with alphanumeric characters.
  10. +
  11. domain should not contain any two consecutive special characters.
  12. +
+
Addressa/ +

Should not be empty.

+
Rater/ +

Should be a number consisting of only digits 0-9 and an optional decimal point.

+

Should not be negative.

+

Should not have more than two decimal places.

+

Should not be larger than 1000000 (one million).

+
Tagt/ +

Contains only alphanumeric characters.

+

Should not be empty.

+
Durationd/ +

Should be a number consisting of only digits 0-9 and an optional decimal point.

+

Should be positive.

+

Should not be larger than 23.99. Consider splitting the job into multiple smaller jobs if a larger value is desired.

+

Should not be empty.

+
Employee to assigni/ +

Contains only digits 0-9.

+

Should be a valid index of an employee in the employee list.

+

Should not be empty.

+
JOB_INDEXN.A. +

Contains only digits 0-9.

+

Should be a valid index of a job in the job list.

+

Should not be empty.

+
EMPLOYEE_INDEXN.A. +

Contains only digits 0-9.

+

Should be a valid index of an employee in the employee list.

+

Should not be empty.

+
+ +[Return to the Table of Contents](#toc) + +-------------------------------------------------------------------------------------------------------------------- + +## Features + +### Employee-related commands + +#### `personadd`: Add an employee + +Adds a new employee to the system with the given attributes. -* 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. +Rate refers to the hourly pay of the employee. + +Format: `personadd n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS r/RATE [t/TAG]...` + +Examples: + +* `personadd n/Nicole Tan p/99338558 e/nicole@stffhub.org a/1 Tech Drive, S138572 r/37.50 t/Hardware t/Senior` will create a new employee with name "Nicole Tan", phone number "99338558", email "nicole@stffhub.org", address "1 Tech Drive, S138572", an hourly rate of $37.50, and with tags "Hardware" and "Senior". +* `personadd n/Jennifer Tan p/88473219 e/jennifer@stffhub.org a/13 Tech Drive, S182562 r/25` will create a new employee with name "Jennifer Tan", phone number "88473219", email "jennifer@stffhub.org", address "13 Tech Drive, S182562", an hourly rate of $25. No tags will be added since no tag attributes were provided. + +![Adding Jennifer](images/screenshots/personadd/personadd.png) + +
+ After adding Jennifer Tan to PeopleSoft +
+ +
-* 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. +**:information_source: Note:**
-* 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. +* Attributes containing `/` are not accepted. Replace `/` with another character (e.g. `-`) if any attribute contains `/`. -* 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`. + For example: Use `Ravi s-o Veegan` instead of `Ravi s/o Veegan`, and `3-5 Jalan Trus` instead of `3/5 Jalan Trus`. + +* The maximum value for the hourly rate of an employee is $1,000,000.
-### Viewing help : `help` +[Return to the Table of Contents](#toc) -Shows a message explaning how to access the help page. +#### `personedit`: Edit an employee's information -![help message](images/helpMessage.png) +Edit the information of an existing employee. Use this in the event that an employee's details change. -Format: `help` +Rate updates will only take effect with jobs that are pending completion; payout amounts for already-completed (i.e. marked with `mark`) jobs will not change. + +Format: `personedit EMPLOYEE_INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [r/RATE] [t/TAG]...​` + +Examples: + +* `personedit 2 p/62353535` changes the second employee's phone number to 62353535. +* `personedit 3 t/Hardware t/Network` changes the third employee's tags to "Hardware" and "Network" instead of their original tags. + +
+ +**:information_source: Note:**
+* Attributes containing `/` are not accepted. Replace `/` with another character (e.g. `-`) if any attribute contains `/`. -### Adding a person: `add` + For example: Use `Ravi s-o Veegan` instead of `Ravi s/o Veegan`, and `3-5 Jalan Trus` instead of `3/5 Jalan Trus`. -Adds a person to the address book. +* The maximum value for the hourly rate of an employee is $1,000,000. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +* When editing tags, new tags will **not** be added to the existing tags. Instead, all existing tags will be replaced by new tags. + +* To clear all tags, add `t/` to the command without specifying any other tags. -
:bulb: **Tip:** -A person can have any number of tags (including 0)
+[Return to the Table of Contents](#toc) + +#### `persondelete`: Delete an employee + +Deletes the employee referred to by the index. This also removes the deleted employee from all associated jobs.
+ +
+ +**:warning: Caution:**
+ +This is irreversible. + +
+ +Format: `persondelete EMPLOYEE_INDEX` + +Example: `persondelete 3` deletes the third employee in the list. + +[Return to the Table of Contents](#toc) + +#### `personfind`: Find employees by name or tag + +Finds all employees that have the given keyword(s) in their names or tags, and lists them in the employee list. + +If multiple keywords are entered, only entries that match **all** keywords are returned. + +Keywords are case-insensitive. + +Format: `personfind KEYWORD [KEYWORDS]...​` + 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` -### Listing all persons : `list` +* `personfind Nicole Hardware` finds all employees with "Nicole" **and** "Hardware" in their name and/or tags. +* `personfind Nicole` finds all employees with "Nicole" in their name and/or tags. +* `personfind Nicole Hardware Display` finds all employees with "Nicole", "Hardware", **and** "Display" in their name and/or tags. -Shows a list of all persons in the address book. -Format: `list` +![before](images/screenshots/personfind/personf_before.png) + +
Before the command is executed: all employees are shown.
+ +![after](images/screenshots/personfind/personf_after.png) + +
After the command is executed: Employees with the "Aircon" tag are shown.
+ +[Return to the Table of Contents](#toc) + +#### `personlist`: List all employees + +Lists all employees added to PeopleSoft. -### Editing a person : `edit` +Format: `personlist` -Edits an existing person in the address book. +Example: `personlist` shows all employees. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +[Return to the Table of Contents](#toc) -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ -* At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +#### `export`: Export jobs done by an employee + +Exports a `.csv` file to the `data` folder, containing the jobs that the employee was assigned to, including: +* job IDs, +* job descriptions, +* job statuses (incomplete, pending payment, paid), +* effective rate(s) for the employee, +* job durations, and +* the amount paid, or to be paid to the employee. + +Format: `export EMPLOYEE_INDEX` + +Example: `export 3` exports the third employee in the list. + +
+ +**:information_source: Note:**
+ +This command updates the job list to show all jobs assigned to that employee. + +
+ +[Return to the Table of Contents](#toc) + +-------------------------------------------------------------------------------------------------------------------- + +### Job-related commands + +#### `add`: Add a job + +Adds a new job with the given attributes. `DURATION` refers to the duration required for the job, in hours. + +Format: `add n/NAME d/DURATION` 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. -### Locating persons by name: `find` +* `add n/Fix HDB Lock d/1` creates a job named "Fix HDB Lock" with a duration of 1 hour. +* `add n/Repair aircon d/4` creates a job named "Repair aircon" with a duration of 4 hours. + +
+ +**:information_source: Note:**
+ +* The maximum value for the duration of a job is 1,000,000 hours. + +* Multiple jobs of the same name can be added. These jobs can then be differentiated by their internal ID, the order in which they were added, and/or the employees that were assigned to it, although it can easily result in confusion. It is thus recommended that a user differentiates jobs through naming to avoid any confusion. + +
+ +[Return to the Table of Contents](#toc) + +#### `find`: Find jobs by name + +Finds all jobs that have the given keyword(s) in their names, and lists them in the employee list. -Finds persons whose names contain any of the given keywords. +If multiple keywords are entered, only entries that match **all** keywords are returned. -Format: `find KEYWORD [MORE_KEYWORDS]` +Keywords are case-insensitive. -* The search is case-insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +Format: `find KEYWORD [KEYWORD]...` Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) -### Deleting a person : `delete` +* `find paint` finds all jobs with "paint" in their names. +* `find paint istana` finds all jobs with "paint" **and** "istana" in their names. + +![before](images/screenshots/find/find.png) + +
+ Entries that match all keywords are found +
+ +[Return to the Table of Contents](#toc) + +#### `list`: List all jobs + +Lists all jobs added to PeopleSoft. + +Format: `list` + +Example: `list` shows all jobs. + +[Return to the Table of Contents](#toc) + +#### `delete`: Delete a job + +Deletes the job with the given index. + +
+ +**:warning: Caution:**
+ +This is irreversible. + +
+ +Format: `delete JOB_INDEX` + +Example: `delete 2` deletes the second job. + +[Return to the Table of Contents](#toc) + +#### `assign`: Assign a job to an employee + +Assigns a job to an employee to indicate that they are to be paid for the job. + +Format: `assign JOB_INDEX i/EMPLOYEE_INDEX [i/EMPLOYEE_INDEX]...` + +Examples: + +* `assign 2 i/3` assigns the second job to the third employee. +* `assign 1 i/5 i/7` assigns the first job to the fifth and seventh employees. + +
+ +**:information_source: Note:**
+ +A job that has been [marked](#mark-mark-or-unmark-a-job-as-done) as completed cannot be assigned. If a job is completed, it makes little sense to assign more employees to it. In the event more employees need to be assigned to a job, unmark the job first before assigning them. + +
+ +![after](images/screenshots/assign/assign.png) + +
+ Assigning the 4th job in the job list to Kavya, the first employee in the employees list +
+ +[Return to the Table of Contents](#toc) + +#### `mark`: Mark or unmark a job as done + +Marks a job as done if it was not already marked as done, or marks a job as undone otherwise. + +Marking a job as done indicates that a job has been completed and is pending payment. Unmarking a job causes the pending payment amounts to be subtracted from assigned employees. + +Jobs are initially **not** marked as done when first created, and have to have at least one employee [assigned](#assign-assign-a-job-to-an-employee) to it before it can be marked. + +
+ +**:information_source: Note:**
-Deletes the specified person from the address book. +The hourly rate(s) paid out to each employee for a job is fixed once the job is marked as done; further changes to any employee's rate will not cause the payout amounts to change. -Format: `delete INDEX` +To update the payout amounts to reflect the new hourly rates, unmark and mark the job again. -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index **must be a positive integer** 1, 2, 3, …​ +
+ +Format: `mark JOB_INDEX` 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. -### Clearing all entries : `clear` +* `mark 1` marks the first job, assuming it is not already marked as done. +* `mark 1` unmarks the first job if it has already been marked as done. + +![Command](images/screenshots/mark/mark.png) + +
+ You can use the mark command to both mark and unmark a job +
+ +[Return to the Table of Contents](#toc) + +#### `pay`: Finalize payments for a job + +Finalizes the payments of a job. A job needs to be [marked](#mark-mark-or-unmark-a-job-as-done) as done before it can be finalized. + +
+ +**:warning: Caution:**
+ +This is irreversible. The finalized job cannot be modified in any way, and can only be removed with [`clear`](#clear-clear-all-peoplesoft-data). + +
+ +Format: `pay JOB_INDEX y/` + +Example: `pay 2 y/` finalizes the payments of the second job + +![before](images/screenshots/pay/before.png) + +
+ Before paying for the second job +
+ +![after](images/screenshots/pay/after.png) -Clears all entries from the address book. +
+ After paying for the second job +
+ +[Return to the Table of Contents](#toc) + +-------------------------------------------------------------------------------------------------------------------- + +### Miscellaneous commands + +#### `clear`: Clear all PeopleSoft data + +Removes **all** data stored in PeopleSoft. Useful for removing the sample data created when PeopleSoft is started for the first time. + +
+ +**:warning: Caution:**
+ +This is irreversible; deleted data cannot be recovered afterwards without a backup. + +
Format: `clear` -### Exiting the program : `exit` +Example: `clear` removes all employees and jobs from PeopleSoft. + +[Return to the Table of Contents](#toc) + +#### `exit`: Exit PeopleSoft -Exits the program. +Exits PeopleSoft immediately. Format: `exit` -### Saving the data +Example: `exit` exits PeopleSoft immediately. -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +[Return to the Table of Contents](#toc) -### Editing the data file +#### `help`: Show help page -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. +Opens the help page, which includes a list of commands, command formats, and example usages. -
: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. -
+Format: `help` + +Example: `help` opens up the help page. + +![help](images/screenshots/help/help.png) -### Archiving data files `[coming in v2.0]` +
+ The help page +
-_Details coming soon ..._ +[Return to the Table of Contents](#toc) -------------------------------------------------------------------------------------------------------------------- ## 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. +**Q**: I'm using macOS and I can't double-click on the `peoplesoft.jar` file to open it. What should I do? + +**A**: Follow the steps [here](https://github.com/nus-cs2103-AY2122S1/forum/issues/353) to open your `jar` file. + +
+ +**Q**: What are the `addressbook.log.0`, `config.json`, `preferences.json` files in the same folder as `peoplesoft.jar`? + +**A**: PeopleSoft uses the settings inside those files to run. For instance, the height and width of PeopleSoft is stored in those files. Do not remove or edit those files. + +
+ +**Q**: How do I save my data? + +**A**: PeopleSoft automatically saves application data to the folder that it was started from after every command. There is no need to save manually. + +
+ +**Q**: How can I edit PeopleSoft data manually? + +**A**: PeopleSoft data is saved as a JSON file under `data/peoplesoft.json` in the folder that it was started from. It is possible (although not recommended) to modify application data by editing that file. + +
+ +**:warning: Caution:**
+ +Do not edit the data directly unless you know what you are doing. If your changes cause the data file to become invalid, PeopleSoft will discard all data and start with an empty data file the next time it is started. + +
+ +
+ +**Q**: Can I get back the initial sample data? + +**A**: Deleting the `data/peoplesoft.json` file and restarting PeopleSoft will cause the sample data to be reloaded. + +
+ +**Q**: How do I transfer my data to another computer? + +**A**: Install PeopleSoft on the other computer and overwrite the `data/peoplesoft.json` file it creates with your existing `data/peoplesoft.json` file. + +
+ +**Q**: How do I report a bug? How do I suggest a feature? + +**A**: You may report a bug or suggest a new feature on the [PeopleSoft issue tracker](https://github.com/AY2122S2-CS2103T-T11-4/tp/issues). + +[Return to the Table of Contents](#toc) -------------------------------------------------------------------------------------------------------------------- -## Command summary +## Glossary + +**CLI**: Command-line interface. A primarily text-based interface, which is typically operated with text commands. + +**CSV**: Comma-separated values. A common file format for storing tabular data, similar to a spreadsheet. + +**GUI**: Graphical user interface. A primarily visual interface, which is typically operated by a pointing device. + +**Index**: The item's number that is displayed in its respective list, e.g. the second employee in the list has an `INDEX` of 2. + +**JAR**: Java ARchive. A file format used to collate Java class files and their resources for distribution. Java applications are commonly distributed as `.jar` files. + +**JSON**: JavaScript Object Notation. A structured file format used to store arbitrary text data. This is the file format used by PeopleSoft to store data and settings. + +**Keyword**: A word to search for in a set of data, e.g. in the list of employees or jobs. -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` +[Return to the Table of Contents](#toc) diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..e92b4ba8b75 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,5 +1,5 @@ -title: "AB-3" -theme: minima +title: "PeopleSoft" +theme: jekyll-theme-cayman header_pages: - UserGuide.md @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2122S2-CS2103T-T11-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..0083697fefb 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: "PeopleSoft"; font-size: 32px; } } diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss index b5ec6976efa..440f5469825 100644 --- a/docs/assets/css/style.scss +++ b/docs/assets/css/style.scss @@ -10,3 +10,7 @@ height: 21px; width: 21px } + +figcaption { + padding-bottom: 16px; +} diff --git a/docs/diagrams/ArchitectureDiagram.puml b/docs/diagrams/ArchitectureDiagram.puml index 4c5cf58212e..7fb4c5d1050 100644 --- a/docs/diagrams/ArchitectureDiagram.puml +++ b/docs/diagrams/ArchitectureDiagram.puml @@ -27,7 +27,7 @@ Main -[#grey]-> Storage Main -up[#grey]-> Model Main -down[hidden]-> Commons -Storage -up[STORAGE_COLOR].> Model -Storage .right[STORAGE_COLOR].>File +Storage -up[STORAGE_COLOR_T3].> Model +Storage .right[STORAGE_COLOR_T3].>File User ..> UI @enduml diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index ef81d18c337..b978d10a268 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -7,10 +7,10 @@ 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 : "persondelete 1" activate ui UI_COLOR -ui -[UI_COLOR]> logic : execute("delete 1") +ui -[UI_COLOR_T2]> logic : execute("persondelete 1") activate logic LOGIC_COLOR logic -[LOGIC_COLOR]> model : deletePerson(p) @@ -22,17 +22,17 @@ deactivate model logic -[LOGIC_COLOR]> storage : saveAddressBook(addressBook) activate storage STORAGE_COLOR -storage -[STORAGE_COLOR]> storage : Save to file +storage -[STORAGE_COLOR_T3]> storage : Save to file activate storage STORAGE_COLOR_T1 -storage --[STORAGE_COLOR]> storage +storage --[STORAGE_COLOR_T3]> storage deactivate storage -storage --[STORAGE_COLOR]> logic +storage --[STORAGE_COLOR_T3]> logic deactivate storage logic --[LOGIC_COLOR]> ui deactivate logic -ui--[UI_COLOR]> user +ui--[UI_COLOR_T2]> user deactivate ui @enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 5731f9cbaa1..e9cc0033d32 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -1,21 +1,28 @@ @startuml !include style.puml skinparam arrowThickness 1.1 -skinparam arrowColor MODEL_COLOR +skinparam arrowColor MODEL_COLOR_T4 skinparam classBackgroundColor MODEL_COLOR -AddressBook *-right-> "1" UniquePersonList -AddressBook *-right-> "1" UniqueTagList -UniqueTagList -[hidden]down- UniquePersonList -UniqueTagList -[hidden]down- UniquePersonList +AddressBook *-left-> "1" UniquePersonList +AddressBook *-right-> "1" UniqueJobList -UniqueTagList *-right-> "*" Tag -UniquePersonList -right-> Person +UniqueJobList *--> Job +UniquePersonList --> Person -Person -up-> "*" Tag +Person - Job +(Person, Job) .. Employment +Person *--> PersonID Person *--> Name Person *--> Phone Person *--> Email Person *--> Address +Person *--> Rate +Person *--> "List" + +Job *--> JobID +Job *--> Description +Job *--> Duration + @enduml diff --git a/docs/diagrams/ComponentManagers.puml b/docs/diagrams/ComponentManagers.puml index 5e907dc1115..3958c9881d6 100644 --- a/docs/diagrams/ComponentManagers.puml +++ b/docs/diagrams/ComponentManagers.puml @@ -3,6 +3,17 @@ skinparam arrowThickness 1.1 skinparam arrowColor LOGIC_COLOR_T4 skinparam classBackgroundColor LOGIC_COLOR +' @@author arrow from nowhere adapted from Anthony-Gaudino +' https://forum.plantuml.net/6378/inwards-outwards-arrow-to-from-a-class-from-to-nowhere +skinparam rectangle { + BorderColor<> transparent + FontColor<> transparent + StereotypeFontColor<> transparent +} + +rectangle invis <> { +} +' @@author package Logic { Class "<>\nLogic" as Logic @@ -19,8 +30,7 @@ Class "<>\nStorage" as Storage Class StorageManager } -Class HiddenOutside #FFFFFF -HiddenOutside ..> Logic +invis ..> Logic LogicManager .up.|> Logic ModelManager .up.|> Model diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 1dc2311b245..bf3f5305a51 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -4,8 +4,8 @@ box Logic LOGIC_COLOR_T1 participant ":LogicManager" as LogicManager LOGIC_COLOR participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR -participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR -participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR +participant ":JobDeleteCommandParser" as JobDeleteCommandParser LOGIC_COLOR +participant "d:JobDeleteCommand" as JobDeleteCommand LOGIC_COLOR participant ":CommandResult" as CommandResult LOGIC_COLOR end box @@ -19,50 +19,50 @@ activate LogicManager LogicManager -> AddressBookParser : parseCommand("delete 1") activate AddressBookParser -create DeleteCommandParser -AddressBookParser -> DeleteCommandParser -activate DeleteCommandParser +create JobDeleteCommandParser +AddressBookParser -> JobDeleteCommandParser +activate JobDeleteCommandParser -DeleteCommandParser --> AddressBookParser -deactivate DeleteCommandParser +JobDeleteCommandParser --> AddressBookParser +deactivate JobDeleteCommandParser -AddressBookParser -> DeleteCommandParser : parse("1") -activate DeleteCommandParser +AddressBookParser -> JobDeleteCommandParser : parse("1") +activate JobDeleteCommandParser -create DeleteCommand -DeleteCommandParser -> DeleteCommand -activate DeleteCommand +create JobDeleteCommand +JobDeleteCommandParser -> JobDeleteCommand +activate JobDeleteCommand -DeleteCommand --> DeleteCommandParser : d -deactivate DeleteCommand +JobDeleteCommand --> JobDeleteCommandParser : d +deactivate JobDeleteCommand -DeleteCommandParser --> AddressBookParser : d -deactivate DeleteCommandParser +JobDeleteCommandParser --> AddressBookParser : d +deactivate JobDeleteCommandParser 'Hidden arrow to position the destroy marker below the end of the activation bar. -DeleteCommandParser -[hidden]-> AddressBookParser -destroy DeleteCommandParser +JobDeleteCommandParser -[hidden]-> AddressBookParser +destroy JobDeleteCommandParser AddressBookParser --> LogicManager : d deactivate AddressBookParser -LogicManager -> DeleteCommand : execute() -activate DeleteCommand +LogicManager -> JobDeleteCommand : execute() +activate JobDeleteCommand -DeleteCommand -> Model : deletePerson(1) +JobDeleteCommand -> Model : deleteJob(1) activate Model -Model --> DeleteCommand +Model --> JobDeleteCommand deactivate Model create CommandResult -DeleteCommand -> CommandResult +JobDeleteCommand -> CommandResult activate CommandResult -CommandResult --> DeleteCommand +CommandResult --> JobDeleteCommand deactivate CommandResult -DeleteCommand --> LogicManager : result -deactivate DeleteCommand +JobDeleteCommand --> LogicManager : result +deactivate JobDeleteCommand [<--LogicManager deactivate LogicManager diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index d4193173e18..092026a2c91 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -4,6 +4,18 @@ skinparam arrowThickness 1.1 skinparam arrowColor LOGIC_COLOR_T4 skinparam classBackgroundColor LOGIC_COLOR +' @@author arrow from nowhere adapted from Anthony-Gaudino +' https://forum.plantuml.net/6378/inwards-outwards-arrow-to-from-a-class-from-to-nowhere +skinparam rectangle { + BorderColor<> transparent + FontColor<> transparent + StereotypeFontColor<> transparent +} + +rectangle arrowFromNowhere <> { +} +' @@author + package Logic { Class AddressBookParser @@ -17,14 +29,13 @@ Class LogicManager } package Model{ -Class HiddenModel #FFFFFF } package Storage{ } -Class HiddenOutside #FFFFFF -HiddenOutside ..> Logic + +arrowFromNowhere ..> Logic LogicManager .right.|> Logic LogicManager -right->"1" AddressBookParser @@ -38,7 +49,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 = JobAddCommand, \nJobFindCommand, etc Logic ..> CommandResult LogicManager .down.> CommandResult diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 4439108973a..65108773019 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -1,7 +1,7 @@ @startuml !include style.puml skinparam arrowThickness 1.1 -skinparam arrowColor MODEL_COLOR +skinparam arrowColor LOGIC_COLOR_T4 skinparam classBackgroundColor MODEL_COLOR Package Model <>{ @@ -14,16 +14,24 @@ Class UserPrefs Class UniquePersonList Class Person -Class Address -Class Email -Class Name -Class Phone -Class Tag +Class UniqueJobList +Class Job } -Class HiddenOutside #FFFFFF -HiddenOutside ..> Model +' @@author arrow from nowhere adapted from Anthony-Gaudino +' https://forum.plantuml.net/6378/inwards-outwards-arrow-to-from-a-class-from-to-nowhere +skinparam rectangle { + BorderColor<> transparent + FontColor<> transparent + StereotypeFontColor<> transparent +} + +rectangle arrowFromNowhere <> { +} +' @@author + +arrowFromNowhere ..> Model AddressBook .up.|> ReadOnlyAddressBook @@ -36,15 +44,10 @@ UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +AddressBook *--> "1" UniqueJobList +UniqueJobList --> "~* all" Job ModelManager -->"~* filtered" Person +ModelManager -->"~* filtered" Job @enduml diff --git a/docs/diagrams/MultiplierTagInheritanceDiagram.puml b/docs/diagrams/MultiplierTagInheritanceDiagram.puml new file mode 100644 index 00000000000..480968e1700 --- /dev/null +++ b/docs/diagrams/MultiplierTagInheritanceDiagram.puml @@ -0,0 +1,19 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor MODEL_COLOR + +Package Tag <>{ +Class Tag +Class MultiplierTag +} + +Tag <|-- MultiplierTag + + +Tag : isValidTagName() +MultiplierTag : Tag[] elementData +MultiplierTag : size() + +@enduml diff --git a/docs/diagrams/ParserClasses.puml b/docs/diagrams/ParserClasses.puml index 0c7424de6e0..e67c4924bc4 100644 --- a/docs/diagrams/ParserClasses.puml +++ b/docs/diagrams/ParserClasses.puml @@ -4,6 +4,18 @@ skinparam arrowThickness 1.1 skinparam arrowColor LOGIC_COLOR_T4 skinparam classBackgroundColor LOGIC_COLOR +' @@author arrow from nowhere adapted from Anthony-Gaudino +' https://forum.plantuml.net/6378/inwards-outwards-arrow-to-from-a-class-from-to-nowhere +skinparam rectangle { + BorderColor<> transparent + FontColor<> transparent + StereotypeFontColor<> transparent +} + +rectangle invis <> { +} +' @@author + Class "{abstract}\nCommand" as Command Class XYZCommand @@ -18,8 +30,7 @@ Class ArgumentTokenizer Class Prefix } -Class HiddenOutside #FFFFFF -HiddenOutside ..> AddressBookParser +invis ..> AddressBookParser AddressBookParser .down.> XYZCommandParser: creates > diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index 760305e0e58..dffdde0ca65 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -1,10 +1,22 @@ @startuml !include style.puml skinparam arrowThickness 1.1 -skinparam arrowColor STORAGE_COLOR +skinparam arrowColor STORAGE_COLOR_T4 skinparam classBackgroundColor STORAGE_COLOR -package Storage{ +' @@author arrow from nowhere adapted from Anthony-Gaudino +' https://forum.plantuml.net/6378/inwards-outwards-arrow-to-from-a-class-from-to-nowhere +skinparam rectangle { + BorderColor<> transparent + FontColor<> transparent + StereotypeFontColor<> transparent +} + +rectangle invis <> { +} +' @@author + +package Storage { package "UserPrefs Storage" #F4F6F6{ Class "<>\nUserPrefsStorage" as UserPrefsStorage @@ -17,27 +29,20 @@ Class StorageManager package "AddressBook Storage" #F4F6F6{ Class "<>\nAddressBookStorage" as AddressBookStorage Class JsonAddressBookStorage -Class JsonSerializableAddressBook -Class JsonAdaptedPerson -Class JsonAdaptedTag } } -Class HiddenOutside #FFFFFF -HiddenOutside ..> Storage +invis ..> Storage StorageManager .up.|> Storage StorageManager -up-> "1" UserPrefsStorage StorageManager -up-> "1" AddressBookStorage -Storage -left-|> UserPrefsStorage -Storage -right-|> AddressBookStorage +Storage --left--|> UserPrefsStorage +Storage --right--|> AddressBookStorage JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage -JsonAddressBookStorage ..> JsonSerializableAddressBook -JsonSerializableAddressBook --> "*" JsonAdaptedPerson -JsonAdaptedPerson --> "*" JsonAdaptedTag @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..bb4e5155e3e 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -4,57 +4,91 @@ skinparam arrowThickness 1.1 skinparam arrowColor UI_COLOR_T4 skinparam classBackgroundColor UI_COLOR +' @@author arrow from nowhere adapted from Anthony-Gaudino +' https://forum.plantuml.net/6378/inwards-outwards-arrow-to-from-a-class-from-to-nowhere +skinparam rectangle { + BorderColor<> transparent + FontColor<> transparent + StereotypeFontColor<> transparent + BorderColor<> transparent + FontColor<> transparent + StereotypeFontColor<> transparent +} + +rectangle arrowFromNowhere <> { +} +' @@author + package UI <>{ Class "<>\nUi" as Ui Class "{abstract}\nUiPart" as UiPart Class UiManager Class MainWindow -Class HelpWindow + +Class SideBar +Class PageSwitcher Class ResultDisplay +Class CommandBox +Class HelpPage +Class OverviewPage +Class "{abstract}\nPage" as Page + +PageSwitcher -[hidden]right- MainWindow +PageSwitcher -[hidden]down- SideBar +(MainWindow, SideBar) .. PageSwitcher Class PersonListPanel Class PersonCard -Class StatusBarFooter -Class CommandBox +Class JobListPanel +Class JobCard } package Model <> { -Class HiddenModel #FFFFFF + ' invisible boxes inside to make the high-level components look bigger + rectangle a <> { + } } package Logic <> { -Class HiddenLogic #FFFFFF + ' invisible boxes inside to make the high-level components look bigger + rectangle b <> { + } } -Class HiddenOutside #FFFFFF -HiddenOutside ..> Ui -UiManager .left.|> Ui +arrowFromNowhere ..> Ui + +UiManager -left-|> Ui UiManager -down-> "1" MainWindow -MainWindow *-down-> "1" CommandBox -MainWindow *-down-> "1" ResultDisplay -MainWindow *-down-> "1" PersonListPanel -MainWindow *-down-> "1" StatusBarFooter -MainWindow --> "0..1" HelpWindow -PersonListPanel -down-> "*" PersonCard +MainWindow *-down-> "1" CommandBox +MainWindow *-down-> "1" ResultDisplay +MainWindow *-down-> "1" SideBar +MainWindow *-down-> "1" HelpPage +MainWindow *-down-> "1" OverviewPage -MainWindow -left-|> UiPart +MainWindow --left--|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart -StatusBarFooter --|> UiPart -HelpWindow --|> UiPart +SideBar --|> UiPart + +OverviewPage --|> Page +HelpPage --|> Page + +Page --|> UiPart + +OverviewPage *-down-> "1" PersonListPanel +OverviewPage *-down-> "1" JobListPanel + +PersonListPanel *-down-> "*" PersonCard +JobListPanel *-down-> "*" JobCard -PersonCard ..> Model +PersonCard .right.> Model +JobCard .right.> Model UiManager -right-> Logic -MainWindow -left-> Logic +MainWindow -right-> Logic -PersonListPanel -[hidden]left- HelpWindow -HelpWindow -[hidden]left- CommandBox -CommandBox -[hidden]left- ResultDisplay -ResultDisplay -[hidden]left- StatusBarFooter +SideBar -[hidden]right- CommandBox +CommandBox -[hidden]right- ResultDisplay -MainWindow -[hidden]-|> UiPart @enduml diff --git a/docs/diagrams/style.css b/docs/diagrams/style.css new file mode 100644 index 00000000000..72c769e9f4e --- /dev/null +++ b/docs/diagrams/style.css @@ -0,0 +1,28 @@ +/** +This is just a preview for me to see the colours in the editor. + */ +.body { + -fx-background-color: #92ffc5; + -fx-background-color: #c5ffe0; + -fx-background-color: #3dff98; + -fx-background-color: #00a34c; + -fx-background-color: #004e25; + + -fx-background-color: #958cff; + -fx-background-color: #d4d0ff; + -fx-background-color: #766aff; + -fx-background-color: #4737ff; + -fx-background-color: #0a007b; + + -fx-background-color: #95c4ff; + -fx-background-color: #c8e0ff; + -fx-background-color: #62a8ff; + -fx-background-color: #4095ff; + -fx-background-color: #003373; + + -fx-background-color: #c3ff8e; + -fx-background-color: #e7ffd2; + -fx-background-color: #9fff4a; + -fx-background-color: #73f400; + -fx-background-color: #336c00; +} diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index fad8b0adeaa..36280ecfbf4 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -7,41 +7,42 @@ 'T1 through T4 are shades of the original color from lightest to darkest -!define UI_COLOR #1D8900 -!define UI_COLOR_T1 #83E769 -!define UI_COLOR_T2 #3FC71B -!define UI_COLOR_T3 #166800 -!define UI_COLOR_T4 #0E4100 +!define UI_COLOR #92ffc5 +!define UI_COLOR_T1 #c5ffe0 +!define UI_COLOR_T2 #3dff98 +!define UI_COLOR_T3 #00a34c +!define UI_COLOR_T4 #004e25 -!define LOGIC_COLOR #3333C4 -!define LOGIC_COLOR_T1 #C8C8FA -!define LOGIC_COLOR_T2 #6A6ADC -!define LOGIC_COLOR_T3 #1616B0 -!define LOGIC_COLOR_T4 #101086 +!define LOGIC_COLOR #958cff +!define LOGIC_COLOR_T1 #d4d0ff +!define LOGIC_COLOR_T2 #766aff +!define LOGIC_COLOR_T3 #4737ff +!define LOGIC_COLOR_T4 #0a007b -!define MODEL_COLOR #9D0012 -!define MODEL_COLOR_T1 #F97181 -!define MODEL_COLOR_T2 #E41F36 -!define MODEL_COLOR_T3 #7B000E -!define MODEL_COLOR_T4 #51000A +!define MODEL_COLOR #95c4ff +!define MODEL_COLOR_T1 #c8e0ff +!define MODEL_COLOR_T2 #62a8ff +!define MODEL_COLOR_T3 #4095ff +!define MODEL_COLOR_T4 #003373 -!define STORAGE_COLOR #A38300 -!define STORAGE_COLOR_T1 #FFE374 -!define STORAGE_COLOR_T2 #EDC520 -!define STORAGE_COLOR_T3 #806600 -!define STORAGE_COLOR_T2 #544400 +!define STORAGE_COLOR #baff7d +!define STORAGE_COLOR_T1 #e7ffd2 +!define STORAGE_COLOR_T2 #9fff4a +!define STORAGE_COLOR_T3 #73f400 +!define STORAGE_COLOR_T4 #336c00 !define USER_COLOR #000000 +!define LINE_COLOR #444444 skinparam BackgroundColor #FFFFFFF skinparam Shadowing false skinparam Class { - FontColor #FFFFFF + FontColor #000000 BorderThickness 1 - BorderColor #FFFFFF - StereotypeFontColor #FFFFFF + BorderColor #000000 + StereotypeFontColor #000000 FontName Arial } @@ -55,15 +56,17 @@ skinparam Sequence { MessageAlign center BoxFontSize 15 BoxPadding 0 - BoxFontColor #FFFFFF + BoxFontColor #000000 FontName Arial } skinparam Participant { - FontColor #FFFFFFF + FontColor #000000 Padding 20 } +skinparam SequenceBoxBorderColor LINE_COLOR +skinparam SequenceLifeLineBorderColor LINE_COLOR skinparam MinClassWidth 50 skinparam ParticipantPadding 10 skinparam Shadowing false diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png index 86c60246ccb..2766154a5e3 100644 Binary files a/docs/images/ArchitectureDiagram.png and b/docs/images/ArchitectureDiagram.png differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 2f1346869d0..b0c277d1d15 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 1ec62caa2a5..d344a58ced3 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/ComponentManagers.png b/docs/images/ComponentManagers.png index b5764ff9273..778c27e62ba 100644 Binary files a/docs/images/ComponentManagers.png and b/docs/images/ComponentManagers.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index fa327b39618..6fe3d170740 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index 9e9ba9f79e5..fa828d37e65 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 04070af60d8..393372dbf2c 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/MultiplierTagInheritanceDiagram.png b/docs/images/MultiplierTagInheritanceDiagram.png new file mode 100644 index 00000000000..1efff7788a8 Binary files /dev/null and b/docs/images/MultiplierTagInheritanceDiagram.png differ diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png index e7b4c8880cd..4fa2a6079ad 100644 Binary files a/docs/images/ParserClasses.png and b/docs/images/ParserClasses.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 2533a5c1af0..bc8185e9ad5 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.gif b/docs/images/Ui.gif new file mode 100644 index 00000000000..b548d928a7c Binary files /dev/null and b/docs/images/Ui.gif differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..a3cbfe79c90 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..aed008dd02c 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoSequenceDiagram.png b/docs/images/UndoSequenceDiagram.png index 6addcd3a8d9..ed07f847f7f 100644 Binary files a/docs/images/UndoSequenceDiagram.png and b/docs/images/UndoSequenceDiagram.png differ diff --git a/docs/images/_AB3_Ui.png b/docs/images/_AB3_Ui.png new file mode 100644 index 00000000000..5bd77847aa2 Binary files /dev/null and b/docs/images/_AB3_Ui.png differ diff --git a/docs/images/apple-tick-emoji.png b/docs/images/apple-tick-emoji.png new file mode 100644 index 00000000000..6c6cfe150ac Binary files /dev/null and b/docs/images/apple-tick-emoji.png differ diff --git a/docs/images/ian-from-dover.png b/docs/images/ian-from-dover.png new file mode 100644 index 00000000000..1d461569a39 Binary files /dev/null and b/docs/images/ian-from-dover.png differ diff --git a/docs/images/ian_sketches.jpg b/docs/images/ian_sketches.jpg new file mode 100644 index 00000000000..25183cb7eb9 Binary files /dev/null and b/docs/images/ian_sketches.jpg differ diff --git a/docs/images/ovidharshini.png b/docs/images/ovidharshini.png new file mode 100644 index 00000000000..11d9dd17265 Binary files /dev/null and b/docs/images/ovidharshini.png differ diff --git a/docs/images/ovidharsini.png b/docs/images/ovidharsini.png new file mode 100644 index 00000000000..1ce7ce16dc8 Binary files /dev/null and b/docs/images/ovidharsini.png differ diff --git a/docs/images/oviya_gui.png b/docs/images/oviya_gui.png new file mode 100644 index 00000000000..edbeac303fb Binary files /dev/null and b/docs/images/oviya_gui.png differ diff --git a/docs/images/screenshots/Ui_label.png b/docs/images/screenshots/Ui_label.png new file mode 100644 index 00000000000..622d8b0653c Binary files /dev/null and b/docs/images/screenshots/Ui_label.png differ diff --git a/docs/images/screenshots/assign/assign.png b/docs/images/screenshots/assign/assign.png new file mode 100644 index 00000000000..48808467481 Binary files /dev/null and b/docs/images/screenshots/assign/assign.png differ diff --git a/docs/images/screenshots/command_format.png b/docs/images/screenshots/command_format.png new file mode 100644 index 00000000000..b2e8ac0a949 Binary files /dev/null and b/docs/images/screenshots/command_format.png differ diff --git a/docs/images/screenshots/data_folder.png b/docs/images/screenshots/data_folder.png new file mode 100644 index 00000000000..fb4372a6ec3 Binary files /dev/null and b/docs/images/screenshots/data_folder.png differ diff --git a/docs/images/screenshots/find/find.png b/docs/images/screenshots/find/find.png new file mode 100644 index 00000000000..fdf7fb2ef37 Binary files /dev/null and b/docs/images/screenshots/find/find.png differ diff --git a/docs/images/screenshots/help/help.png b/docs/images/screenshots/help/help.png new file mode 100644 index 00000000000..4ee88c91304 Binary files /dev/null and b/docs/images/screenshots/help/help.png differ diff --git a/docs/images/screenshots/mark/mark.png b/docs/images/screenshots/mark/mark.png new file mode 100644 index 00000000000..dab8bf3e1ce Binary files /dev/null and b/docs/images/screenshots/mark/mark.png differ diff --git a/docs/images/screenshots/pay/after.png b/docs/images/screenshots/pay/after.png new file mode 100644 index 00000000000..3c8132b3f18 Binary files /dev/null and b/docs/images/screenshots/pay/after.png differ diff --git a/docs/images/screenshots/pay/before.png b/docs/images/screenshots/pay/before.png new file mode 100644 index 00000000000..bb102321a57 Binary files /dev/null and b/docs/images/screenshots/pay/before.png differ diff --git a/docs/images/screenshots/personadd/personadd.png b/docs/images/screenshots/personadd/personadd.png new file mode 100644 index 00000000000..f47a913f9a9 Binary files /dev/null and b/docs/images/screenshots/personadd/personadd.png differ diff --git a/docs/images/screenshots/personfind/personf_after.png b/docs/images/screenshots/personfind/personf_after.png new file mode 100644 index 00000000000..9d9ed3ec47c Binary files /dev/null and b/docs/images/screenshots/personfind/personf_after.png differ diff --git a/docs/images/screenshots/personfind/personf_before.png b/docs/images/screenshots/personfind/personf_before.png new file mode 100644 index 00000000000..81ef73f4182 Binary files /dev/null and b/docs/images/screenshots/personfind/personf_before.png differ diff --git a/docs/images/screenshots/tut_add.png b/docs/images/screenshots/tut_add.png new file mode 100644 index 00000000000..0aa020f8dc8 Binary files /dev/null and b/docs/images/screenshots/tut_add.png differ diff --git a/docs/images/screenshots/tut_asgn.png b/docs/images/screenshots/tut_asgn.png new file mode 100644 index 00000000000..2e4ac94bd14 Binary files /dev/null and b/docs/images/screenshots/tut_asgn.png differ diff --git a/docs/images/screenshots/tut_find.png b/docs/images/screenshots/tut_find.png new file mode 100644 index 00000000000..91346ab63a4 Binary files /dev/null and b/docs/images/screenshots/tut_find.png differ diff --git a/docs/images/screenshots/tut_markpay.png b/docs/images/screenshots/tut_markpay.png new file mode 100644 index 00000000000..b0f6cb1973e Binary files /dev/null and b/docs/images/screenshots/tut_markpay.png differ diff --git a/docs/images/spyobird.png b/docs/images/spyobird.png new file mode 100644 index 00000000000..2ce8135c875 Binary files /dev/null and b/docs/images/spyobird.png differ diff --git a/docs/images/thewrik.png b/docs/images/thewrik.png new file mode 100644 index 00000000000..ac940acf168 Binary files /dev/null and b/docs/images/thewrik.png differ diff --git a/docs/images/tracing/LogicSequenceDiagram.png b/docs/images/tracing/LogicSequenceDiagram.png index c9b1f6cc232..31f7f948ff6 100644 Binary files a/docs/images/tracing/LogicSequenceDiagram.png and b/docs/images/tracing/LogicSequenceDiagram.png differ diff --git a/docs/images/zhongfu.png b/docs/images/zhongfu.png new file mode 100644 index 00000000000..1ce7ce16dc8 Binary files /dev/null and b/docs/images/zhongfu.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..ebda2a9727f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,23 @@ --- layout: page -title: AddressBook Level-3 +title: PeopleSoft --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![CI Status](https://github.com/AY2122S2-CS2103T-T11-4/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2122S2-CS2103T-T11-4/tp/actions/workflows/gradle.yml) +[![Codecov](https://codecov.io/gh/AY2122S2-CS2103T-T11-4/tp/branch/master/graph/badge.svg?token=Z0PQIQXY29)](https://codecov.io/gh/AY2122S2-CS2103T-T11-4/tp) -![Ui](images/Ui.png) +![Ui](images/Ui.gif) -**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). +PeopleSoft is a CLI-based contractor payroll management app. It helps **companies which offer contractor services** with managing how much each contractor is paid. You can: +- manage contractors +- manage jobs +- calculate monthly salary + +It is written with the OOP paradigm in mind and has ~6 KLoC. +* View the User Guide and Developer Guide on our **[Website](https://ay2122s2-cs2103t-t11-4.github.io/tp/)**. + +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). -* 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** diff --git a/docs/team/ian-from-dover.md b/docs/team/ian-from-dover.md new file mode 100644 index 00000000000..089477f39cc --- /dev/null +++ b/docs/team/ian-from-dover.md @@ -0,0 +1,75 @@ +--- +layout: page +title: Ian Hong's Project Portfolio Page +--- + +### Project: PeopleSoft + +PeopleSoft is a Payroll management app for companies handling contractor-based services. + + +### Summary of Contributions + +* **Role in team**: Team leader and frontend developer +
+* **New Feature**: Added the ability to open the User Guide directly in the web browser. (Pull Request(PR) [\#77](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/77)) + * **What it does**: Allows the user to open the User Guide directly in the web browser. + * **Justification**: This feature improves the product because it significantly reduces the number of steps a user needs to do to search for help. It also follows Jakob Nielsen's Revised Usability Heuristic H2-10: Help and documentation. + * Credits: [Dave from StackOverflow](https://stackoverflow.com/questions/5226212/how-to-open-the-default-webbrowser-using-java/54869038#54869038) for opening the browser across OSes +
+ +* **New Feature**: Added the sidebar and allowed the user to navigate between pages either using it, or using commands. (PR [\#222](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/222)) + * **What it does**: Gives the user multiple ways to change between pages (through both GUI and CLI) + * **Justification**: This feature improves the product because it allows those unfamiliar with CLIs to easily navigate the app, and also provides an accelerator for experienced users (through typing) to navigate to their desired page. It also follows Jakob Nielsen's Revised Usability Heuristic H2-7: Flexibility & Efficiency. + * **Highlights**: The implementation required the use of an association class. Comments left will guide a new collaborator in adding additional pages. +
+ +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=ian-from-dover&sort=groupTitle&sortWithin=title&since=2022-02-18&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&tabOpen=true&tabType=authorship&zFR=false&tabAuthor=ian-from-dover&tabRepo=AY2122S2-CS2103T-T11-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false) +
+ +* **Team tasks**: + * Reviewed [44 PRs](https://github.com/AY2122S2-CS2103T-T11-4/tp/pulls?q=is%3Apr+is%3Aclosed+reviewed-by%3Aian-from-dover+) and offered non-trivial comments. (Eg. PR [\#235](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/235), [\#52](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/52), [\#218](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/218)) + * Ensured that issues and internal milestones were on schedule + * Drafted meeting agendas and facilitated meeting discussion + * Regularly updated tP deliverables document and served as point of contact with tutor + * Made the release `v1.3.0` on GitHub +
+ +* **Enhancements to existing features**: + * Beautified the GUI and DG color schemes (PR [\#61](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/61), [\#221](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/221)) + * Implemented the displaying of almost all the features, including and not limited to: + * `assign`ing people to jobs (PR [\#216](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/216)) + * Making the ResultDisplay language HR-manager friendly (PR [\#214](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/214)) + * Create new fields belonging to `People` such as `basePay` (PR [\#205](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/205)) + * Created the `JobListCards` and `JobListPanel` to display all the job information (PR [\#61](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/61) and [\#125](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/125)) + * Fixed bugs relating to all of the above. +
+ +* **Documentation**: + * User Guide: + * Added the `How to use this guide` section + * Added documentation for the features `job` `joblist`, `jobdelete` and `assign` (PR [\#86](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/86/files)) + * Added the `Glossary` section (PR [\#86](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/86/files) also) + * Updated the command summary with the new job commands + * Did cosmetic tweaks to existing documentation for the `mark` feature: (PR [\#118](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/118/)) + * Extensively researched 20 previous UGs to consolidate best practices for the team. (PR [\#203](https://github.com/AY2122S2-CS2103T-T11-4/tp/issues/203)) + * Developer Guide: + * All changes are in PR [\#221](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/221): + * Added introduction, target user profile and value proposition to the front + * Added implementation details of the GUI under the [UI component section](https://ay2122s2-cs2103t-t11-4.github.io/tp/DeveloperGuide.html). + ![UI Class Diagram](../images/UiClassDiagram.png) + * Tweaked User Stories + * README: + * Added animated GIF preview and crafted the write-up for skimming. (PR [\#36](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/36) and [\#222](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/222)) +
+ +* **Community**: + * Contributed to [3 forum discussions](https://github.com/nus-cs2103-AY2122S2/forum/issues?q=is%3Aissue+author%3Aian-from-dover) + * Reported [10 bugs and suggestions for other teams](https://github.com/ian-from-dover/ped/issues) in the class during Practical Exam Dry run (PE-D) +
+ +* **Tools**: + * Implemented a UI Kit found in Figma by emulating its design using JavaFX + * Touched up all icons and images in Adobe Photoshop + * Brainstormed multiple user interfaces by drawing wireframe sketches before chancing upon the UI Kit +![Sketches](../images/ian_sketches.jpg) diff --git a/docs/team/ovidharshini.md b/docs/team/ovidharshini.md new file mode 100644 index 00000000000..d0806edca0d --- /dev/null +++ b/docs/team/ovidharshini.md @@ -0,0 +1,42 @@ +--- +layout: page + +title: Oviya's Project Portfolio Page +--- + +### Project: PeopleSoft + +PeopleSoft is a CLI-based payroll management app for companies handling contractor-based services. + +Given below are my contributions to the project. + +* **New Feature**: Display of an overview of commands on the HelpWindow page + * What it does: Allow for the display of messages from all command classes on a singular UI component. + * Justification: In previous iterations, the HelpWindow contained a button that linked to the user guide on Github. While this technically fits the wording of the `help` command provided in the user guide, displaying the commands directly would be more user-friendly. + * Highlights: A previous implementation was considered following the implementation for `Person` with similar `CommandHelpMessageCard` and `CommandHelpMessageListPanel` classes. TableView was selectively chosen over this implementation given its relative ease of use. + * Credits: [Amos Chepchieng from Medium](https://medium.com/@keeptoo/adding-data-to-javafx-tableview-stepwise-df582acbae4f) for populating TableView with data programmatically + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=ovidharshini&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-02-18) + +* **Team tasks**: + * Reviewed [16 PRs](https://github.com/AY2122S2-CS2103T-T11-4/tp/pulls?q=type%3Apr+reviewed-by%3Aovidharshini+) and offered non-trivial comments. (Eg. PR [\#225](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/225)) + +* **Enhancements to existing features**: + * Added checks for input validation of `Rate` and `Duration` [\#79](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/79) + * Handle the addition of duplicate employees(PR [\#217](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/217)) + +* **Documentation**: + * User Guide: + * Updated command names after refactoring code + * Fixed error with command summary display + * Developer Guide: + * Added user stories [\#233](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/233), [\#37](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/37) + * Added proposed feature of pay multipliers [\#237](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/237) + * Made cosmetic edits of various parts of DG, and adapted parts of AB-3 to better fit PeopleSoft. [\#233](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/233) + * Added use cases + +* **Community**: + * Submitted [16 bugs](https://github.com/AY2122S2-CS2103T-T09-2/tp/issues?q=ovidharshini) during the PE dry-run + +* **Tools**: + * Sketched a [mockup](images/oviya_gui.png) of a possible graphical user interface in one of the first few weeks. diff --git a/docs/team/spyobird.md b/docs/team/spyobird.md new file mode 100644 index 00000000000..5df1719c07b --- /dev/null +++ b/docs/team/spyobird.md @@ -0,0 +1,76 @@ +--- +layout: page +title: Elliot's Project Portfolio Page +--- + +## Project: PeopleSoft + +PeopleSoft is a payroll management app for companies handling contractor-based services. + +### Summary of Contributions + +Here are some ways I have contributed to this project. + +* **New Feature**: Added basic create and delete functionality for jobs. [\#81](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/81) + * Details: Added `add` and `delete` commands which allows the user to create and delete new jobs + respectively. + * Justification: These are basic features which are necessary for a payroll management application. +* **New Feature**: Added basic search functionality for jobs. [\#109](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/109) + * Details: Added `list` which allows users to see all the jobs that have been stored. Also added `find` to + allow users to search for jobs by keyword queries. + * Justification: As much of the application relies on the state of the jobs being displayed on the UI, these + features were necessary to help the user search through jobs to be able to use other features of the + application more efficiently. +* **New Feature**: Added the functionality of marking jobs as completed. [\#81](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/81) + * Details: Added `mark` which allows users to mark a job as completed. + * Justification: As a payroll management application, the state of the completion of a job needs to be + tracked. This feature models how there can be synchronous state change of jobs when they are completed + in the real-world. However, the ability to reverse the completed state is also available in the event of + user errors. +* **New Feature**: Added the functionality of assigning jobs to persons. [\#81](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/81) + * Details: Added `assign` which allows users to assign a job to one or more persons. + * Justification: As a payroll management application, the association between jobs and persons is important. + It was necessary to include this association which models the real-world where one or more persons can + take on jobs. +* **New Feature**: Added the functionality of finalizing payments for jobs. [\#128](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/128) + * Details: Added `pay` which allows users to finalize payments for a job. + * Justification: As a payroll management application, one defining feature to have is the ability to track + payments. As this application is created to simplify the process of managing and calculating pay, it was + necessary to add this feature to fulfill the product specifications of payroll management software. +* **New Implementation**: Added base implementation for jobs. [\#46](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/46) + * Details: Adds a simple abstraction for a job, including its related attributes, encapsulated within + the `Job` class. + * Justification: As the application is a payroll management application built off a basic address book + design, it was initially missing an implementation for jobs. It was necessary to add a base + implementation for jobs in order to fulfill the basic product specifications of payroll management + software, which includes managing jobs. +* **New Implementation**: Added associations for jobs and persons. [\#81](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/81), [\#106](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/106) + * Details: Adds an abstraction for the association between jobs and persons, encapsulated within the + `Employment` class. + * Justification: Similar to how people can take on jobs in the real-world, the role of `Employment` is to + model this same association that people can have with taking on jobs. This is the framework for which + certain functionality such as `assign` are built upon. +* **Enhancement**: Added implementation for the generation of job IDs. [\#81](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/81) + * Details: Job IDs play the role of uniquely identifying jobs internally. This allows for the assignment of + unique job IDs in the creation of new jobs. +* **Enhancement**: Added logic for the creation and management of payment objects. [\#128](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/128) + * Details: As `Payment` objects are used internally to calculate the actual payment of a person, some logic + was required in the creation and management of these `Payment` objects. +* **Enhancement**: Supported in adding internal JSON serialization/deserialization sub-classes for + job-related classes. [\#81](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/81) + * Details: As PeopleSoft creates local data to store the state of the application, some implementations + were added to handle JSON serialization/deserialization of some classes. +* **Enhancement**: Added some tests for automated testing of new features. (e.g. [\#109](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/109)) + * Details: Some tests were added to increase the test coverage of new features, including but not limited to + those mentioned above. The tests give some assurance of correctness in the main functionality and error + handling, and helps to manage regressions caused by new changes. +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2022-02-18&tabOpen=true&tabType=authorship&tabAuthor=Spyobird&tabRepo=AY2122S2-CS2103T-T11-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false) +* **Project management**: Managed Continuous Integration (CI) of team repository. +* **Team tasks**: + * Reviewed [23 pull requests](https://github.com/AY2122S2-CS2103T-T11-4/tp/pulls?q=type%3Apr+reviewed-by%3Aspyobird) for the team repository. +* **Documentation**: + * User Guide: Contributed to the Quick Start tutorial and minor changes. [\#234](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/234) + * Developer Guide: Contributed to the `Logic` and `Model` components. [\#240](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/240) +* **Community**: + * Participated in [14 forum posts](https://github.com/nus-cs2103-AY2122S2/forum/issues?q=commenter%3Aspyobird) for the module. + * Reported [11 issues](https://github.com/Spyobird/ped/issues) for other team projects during practical exam dry-run. diff --git a/docs/team/thewrik.md b/docs/team/thewrik.md new file mode 100644 index 00000000000..f5d95b74784 --- /dev/null +++ b/docs/team/thewrik.md @@ -0,0 +1,43 @@ +--- +layout: page +title: Wrik's Project Portfolio Page +--- +### Project: PeopleSoft + +PeopleSoft is a Payroll management app for companies handling contractor-based services. + +### Summary of Contributions + +* **Role in team**: Developer, Documentation, Testing + +* **New Feature**: Added the export feature for payslips. + * **What it does**: Gives the user ability to view exact earnings and job assignments. + * **Justification**: Helps contractors keep track of their earnings transparently. + +* **New Feature**: Created the job list interface and a skeletal implementation + * **What it does**: Core functionality of the product. + * **Justification**: Helps contractors keep track of their earnings transparently. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=thewrik) + +* **Enhancements to existing features**: + * Find Functionality + * **Originally** no wildcard support + * After development, finds all people by a certain name and/or tag. If multiple keywords are queried, only entries + that match **all** tags are returned. + * The motivation behind this logic is that users want to increasingly narrow down the number + of persons they see in the outlay. + * Feature was later extended to similar behaviour on jobs as well. + +* **Documentation**: + * User Guide: + * Set up the original draft adapting from the meeting notes. + * Detailed relevant features, specifically elaborated on `find`, `list`, and `export` functionalities. + +* **Testing** + * Wrote the testing suite for Payments. + * Wrote tests for `personfind` and `joblist`. + +* **Community**: + * Reported [6 bugs and suggestions for other teams](https://github.com/thewrik/ped/issues) in the class during Practical Exam Dry run (PE-D) + diff --git a/docs/team/zhongfu.md b/docs/team/zhongfu.md new file mode 100644 index 00000000000..2debdd0bdf8 --- /dev/null +++ b/docs/team/zhongfu.md @@ -0,0 +1,52 @@ +--- +layout: page +title: Zhongfu's Project Portfolio Page +--- + +### Project: PeopleSoft + +PeopleSoft is a CLI-based contractor payroll management app. It helps companies which offer contractor services with managing how much each contractor is paid. You can: + +- manage contractors +- manage jobs +- calculate monthly salary + +Given below are my contributions to the project. + +* **New Feature**: Reusable styled table component for tables in app (part of [\#215](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/215)) + * What it does: Implements a reusable JavaFX control component that provides a table styled according to the intended design of the app. + * Justification: At the moment, there are three tables used in the app, all of which have practically the same styling. However, each of them are implemented separately, which results in redundant code and inconsistencies when adjusting table styling, etc. + * Highlights: Some workarounds had to be used in order to allow for the component to be easily used and instantiated in FXML files. Additionally, a significant amount of time had to be allocated to testing in multiple environments to ensure that the control appears as expected in all scenarios. +

+* **New Feature**: New model classes and supporting code required to support planned features (e.g. Payments, IDs) + * What it does: Allow some new entities to be represented as objects, and allow for relationships not easily represented with objects + * Justification: These model classes are required for supporting new features. Additionally, the planned features require many entity relationships for which having multiple references to the same object(s) would be troublesome, compounded by the fact that new objects are created when editing existing entities. + * Highlights: Some care had to be taken when deciding where IDs were to be used, as overzealous use may negate some of the benefits of OOP. +

+* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s2.github.io/tp-dashboard/?search=zhongfu) +

+* **Team tasks**: + * Handled initial triage for PE-D issues, and followed up on PRs and old issues + * Reviewed [24 pull requests](https://github.com/AY2122S2-CS2103T-T11-4/tp/pulls?q=reviewed-by%3Azhongfu) +

+* **Enhancements to existing feature**: + * New serializer/deserializer architecture for model classes [\#65](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/65), [\#88](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/88) + * What it does: (De)serialize model classes to/from JSON with custom (de)serializers (bundled together with the model classes, as nested classes). + * Justification: The previous (de)serialization mechanism is not as intuitive for new developers, as the relevant classes are spread across multiple packages and types reused in multiple classes cannot be automatically (de)serialized. It also makes implementing novel features (e.g. backward-compatibility) more difficult. This enhancement aims to alleviate some of those issues by allowing for increased flexibility, and by bundling the deser classes with the associated object classes. + * Highlights: Deciding on an architecture that serves all our existing needs (and allows for easy extensibility) was somewhat complicated, as Jackson is a complex library that (as a result) presents many ways to perform any given task. + * GUI bug fixes: Allow window size to increase/decrease w/ elements resizing to fit [#218](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/218), tag wrapping [#209](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/209) + * Quality-of-life bug fixes (e.g. email regex changes [#69](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/69), `Desktop.browse()` hanging on Linux [#208](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/208)) +

+* **Documentation**: + * User Guide: + * Changes to reflect feature changes and fix breakages (e.g. [\#200](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/200)) + * Developer Guide: + * Explain serdes architecture and write tutorials on writing new serdes classes [\#87](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/87), [\#89](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/89) +

+* **Community**: + * Participated in [13 forum discussions](https://github.com/nus-cs2103-AY2122S2/forum/issues?q=commenter%3Azhongfu) + * Submitted [11 bugs](https://github.com/AY2122S2-CS2103T-T13-2/tp/issues?q=zhongfu) during the PE dry-run, 10 of which were accepted as-is (except for a [very-low severity bug](https://github.com/AY2122S2-CS2103T-T13-2/tp/issues/169) eventually marked as a documentation bug) +

+* **Tools**: + * Added git pre-commit hooks that enforce correct style and all tests passing, as well as fix common issues (e.g. incorrect FXML versions caused by SceneBuilder [\#212](https://github.com/AY2122S2-CS2103T-T11-4/tp/pull/212)) + * Set up the official GitHub Telegram notification bot to post notifications of repo comments/new PRs/new commits to the team group chat diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md index 880c701042f..6df033d4a28 100644 --- a/docs/tutorials/AddRemark.md +++ b/docs/tutorials/AddRemark.md @@ -16,16 +16,16 @@ We’ll assume that you have already set up the development environment as outli Looking in the `logic.command` package, you will notice that each existing command have their own class. All the commands inherit from the abstract class `Command` which means that they must override `execute()`. Each `Command` returns an instance of `CommandResult` upon success and `CommandResult#feedbackToUser` is printed to the `ResultDisplay`. -Let’s start by creating a new `RemarkCommand` class in the `src/main/java/seedu/address/logic/command` directory. +Let’s start by creating a new `RemarkCommand` class in the `src/main/java/peoplesoft/logic/command` directory. For now, let’s keep `RemarkCommand` as simple as possible and print some output. We accomplish that by returning a `CommandResult` with an accompanying message. **`RemarkCommand.java`:** ``` java -package seedu.address.logic.commands; +package peoplesoft.logic.commands; -import seedu.address.model.Model; +import peoplesoft.model.Model; /** * Changes the remark of an existing person in the address book. @@ -68,7 +68,7 @@ Following the convention in other commands, we add relevant messages as constant + ": Edits the remark of the person identified " + "by the index number used in the last person listing. " + "Existing remark will be overwritten by the input.\n" - + "Parameters: INDEX (must be a positive integer) " + + "Format: INDEX (must be a positive integer) " + "r/ [REMARK]\n" + "Example: " + COMMAND_WORD + " 1 " + "r/ Likes to swim."; @@ -91,7 +91,7 @@ Let’s change `RemarkCommand` to parse input from the user. We start by modifying the constructor of `RemarkCommand` to accept an `Index` and a `String`. While we are at it, let’s change the error message to echo the values. While this is not a replacement for tests, it is an obvious way to tell if our code is functioning as intended. ``` java -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; //... public class RemarkCommand extends Command { //... @@ -142,7 +142,7 @@ Your code should look something like [this](https://github.com/se-edu/addressboo Now let’s move on to writing a parser that will extract the index and remark from the input provided by the user. -Create a `RemarkCommandParser` class in the `seedu.address.logic.parser` package. The class must extend the `Parser` interface. +Create a `RemarkCommandParser` class in the `peoplesoft.logic.parser` package. The class must extend the `Parser` interface. ![The relationship between Parser and RemarkCommandParser](../images/add-remark/ParserInterface.png) @@ -229,7 +229,7 @@ Now that we have all the information that we need, let’s lay the groundwork fo ### 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 `peoplesoft.model.person`. 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. @@ -242,7 +242,7 @@ Let’s change `RemarkCommand` and `RemarkCommandParser` to use the new `Remark` 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. -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 [`peoplesoft.ui.regions.PersonCard`](https://github.com/se-edu/addressbook-level3/commit/850b78879582f38accb05dd20c245963c65ea599#diff-639834f1e05afe2276a86372adf0fe5f69314642c2d93cfa543d614ce5a76688). **`PersonCard.java`:** diff --git a/docs/tutorials/RemovingFields.md b/docs/tutorials/RemovingFields.md index f29169bc924..105f02c0896 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 `peoplesoft.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. * :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) diff --git a/docs/tutorials/TracingCode.md b/docs/tutorials/TracingCode.md index 4fb62a83ef6..14452a3cff7 100644 --- a/docs/tutorials/TracingCode.md +++ b/docs/tutorials/TracingCode.md @@ -39,7 +39,7 @@ In our case, we would want to begin the tracing at the very point where the App -According to the sequence diagram you saw earlier (and repeated above for reference), the `UI` component yields control to the `Logic` component through a method named `execute`. Searching through the code base for an `execute()` method that belongs to the `Logic` component yields a promising candidate in `seedu.address.logic.Logic`. +According to the sequence diagram you saw earlier (and repeated above for reference), the `UI` component yields control to the `Logic` component through a method named `execute`. Searching through the code base for an `execute()` method that belongs to the `Logic` component yields a promising candidate in `peoplesoft.logic.Logic`. @@ -48,7 +48,7 @@ According to the sequence diagram you saw earlier (and repeated above for refere :bulb: **Intellij Tip:** The ['**Search Everywhere**' feature](https://www.jetbrains.com/help/idea/searching-everywhere.html) can be used here. In particular, the '**Find Symbol**' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a _method_ named `execute`, not simply the text `execute`.
-A quick look at the `seedu.address.logic.Logic` (an extract given below) confirms that this indeed might be what we’re looking for. +A quick look at the `peoplesoft.logic.Logic` (an extract given below) confirms that this indeed might be what we’re looking for. ```java public interface Logic { diff --git a/docs/tutorials/WritingSerdesClasses.md b/docs/tutorials/WritingSerdesClasses.md new file mode 100644 index 00000000000..4a156cef41f --- /dev/null +++ b/docs/tutorials/WritingSerdesClasses.md @@ -0,0 +1,393 @@ +--- +layout: page +title: "Tutorial: Writing custom serializers/deserializers for new model classes" +--- + +So you've just written a new model class, and you want to load and store instances of it from JSON. Obviously, you'd need a way to serialize them to, and deserialize them from JSON. + +# So, why custom serializers/deserializers? + +There's a lot of ways to serialize/deserialize objects with Jackson (such as using the `@JsonCreator` annotation on classes), but the more commonly used methods have some downsides, e.g. not allowing for extra validation to be performed easily. To fix that, the developers of AddressBook-Level3 seems to have opted for a second set of classes (such as `JsonAdaptedPerson`) solely for making serialization and deserialization easier. + +For AB3, serialization from `Person` classes to JSON is rather straightforward -- create a new `JsonAdaptedPerson` instance from an existing `Person` class (which will convert all the fields to types that closely resemble JSON types, such as `String`s for strings, and `List`s for arrays), and have Jackson automatically serialize all fields present in the "adapted" instance. + +Deserialization is similar -- have Jackson create a new `JsonAdaptedPerson` instance automatically, then call a method on the new instance which performs the desired validation checks and creates the actual `Person` instance. + +For PeopleSoft, we've elected to use custom serializers and deserializers for model classes to avoid extraneous classes spread out across multiple packages, retain flexibility in choosing how to serialize and deserialize instances (including validating data), and to make adding model classes more intuitive (as everything you have to do is contained within one class). + +# Implementing serializers and deserializers for new model classes + +For this example, we will be implementing a class named `Foo`. It contains fields of various types, such as: +* primitive types, +* custom, non-generic types (with custom serializers), +* other non-generic types, and +* generic types. + +## Step 1: Add `@JsonSerialize` and `@JsonDeserialize` annotations to the class + +These annotations tell Jackson to use the `FooSerializer` and `FooDeserializer` nested classes (which will be implemented later) to serialize and deserialize `Foo` instances. + +```java +@JsonSerialize(using = Foo.FooSerializer.class) +@JsonDeserialize(using = Foo.FooDeserializer.class) +public class Foo { + private int id; + private Name name; + private BigDecimal num; + private Set tags; +} +``` + +## Step 2: Add the boilerplate code for the `FooSerializer` and `FooDeserializer` classes (as nested classes within `Foo`) + +The skeleton code for the serdes classes are largely similar; the main things that you should be looking at are: +* the type arguments for the classes that our serdes classes inherit from, +* the `serialize()` and `deserialize()` methods, which contain the main logic for serdes, +* the error messages (and error message formatter) in `FooDeserializer`, +* and `FooDeserializer.getNullValue()`, which is called when a JSON `null` value is encountered. (We will typically want to throw an exception here, but you can choose to do something else, e.g. return a default instance) + +```java +@JsonSerialize(using = Foo.FooSerializer.class) +@JsonDeserialize(using = Foo.FooDeserializer.class) +public class Foo { + private int id; + private Name name; + private BigDecimal num; + private Set tags; + + ... + + protected static class FooSerializer extends StdSerializer { + private FooSerializer(Class val) { + super(val); + } + + private FooSerializer() { + this(null); + } + + @Override + public void serialize(Foo val, JsonGenerator gen, SerializerProvider provider) throws IOException { + // TODO + } + } + + protected static class FooDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The Foo instance is invalid or missing!"; + private static final UnaryOperator INVALID_VAL_FMTR = + k -> String.format("This Foo instance's %s value is invalid!", k); + + private FooDeserializer(Class vc) { + super(vc); + } + + private FooDeserializer() { + this(null); + } + + private static JsonNode getNonNullNode(ObjectNode node, String key, DeserializationContext ctx) + throws JsonMappingException { + return JsonUtil.getNonNullNode(node, key, ctx, INVALID_VAL_FMTR); + } + + private static T getNonNullNodeWithType(ObjectNode node, String key, DeserializationContext ctx, + Class cls) throws JsonMappingException { + return JsonUtil.getNonNullNodeWithType(node, key, ctx, INVALID_VAL_FMTR, cls); + } + + @Override + public Foo deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + // TODO + } + + @Override + public Foo getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} +``` + +## Step 3: Decide how you would like to represent object instances (including fields) + +You should decide how each instance should be represented (as a JSON value). + +For simpler model classes, you may be able to represent them as a primitive JSON value -- for instance, a `Name` instance (which in this case, consists of only a single `String` field) could be represented as a single JSON string. + +For slightly more complex (i.e. compound) types, you may want to serialize them into JSON arrays or objects. For instance, you might want to serialize a `Set` object into a JSON array (of serialized `Tag` instances), or a `Foo` into a JSON object: + +```json +{ + "id": 5, + "name": "Nicole Tan", + "num": "1.51", + "tags": ["Intern", "Aircon"] +} +``` + +In this case, we've stored each field in this `Foo` instance as a key-value pair in the resulting JSON object. The key is set to the name of the field, while the value is the serialized JSON representation of the field value. + +Your chosen representation format should be non-ambiguous to ease the implementation of serdes classes. The above would be one such example -- each key corresponds to a field in the object, and each value is serialized and deserialized as a fixed type (e.g. `Name`, `Set`). + +## Step 4: Implement the serializer for the class + +This will generally depend on the representation you've decided on. + +### 4a: Simple model classes (with only one field) + +For simpler types (e.g. those that only consist of one field), you may want to use the methods defined in `com.fasterxml.jackson.core.JsonGenerator` (i.e. the `gen` parameter of `FooSerializer.serialize`), such as: + +* `gen.writeString(String)` +* `gen.writeNumber(BigDecimal)` +* `gen.writeObject(Object)` +* and so on + +A serializer for `Name` instances might then look like: + +```java + @Override + public void serialize(Foo val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(val.getName()); + } +``` + +And for collections (such as `List` and `Set`, and perhaps `Map`), you may be able to use `gen.writeObject(Object)` instead -- this will typically deserialize them into an appropriate representation, such as a JSON array for `List`s and `Set`s, and JSON objects for `Map`s. There should be no need to write custom (de)serializers for typical Java collections, although you will likely want to write serializers for any custom contained type (e.g. `Tag`, for `Set` instances). + +### 4b: Compound model classes (with multiple fields) + +For types that are *compound* types (e.g. those that consist of multiple fields or values), you will likely want to serialize them as an object (or in some cases, arrays). + +You'll need to begin the object or array with the appropriate marker: + +* `gen.writeStartObject()` +* `gen.writeStartArray()` + +Then, for arrays, you can simply write the values one by one using `gen.writeString()` and similar methods. (See [4a: Simple types](#4a-simple-types) for more info.) + +For objects, you will want to write fields. Methods that you can use for this include, but are not limited to: + +* `gen.writeStringField(String, String)` +* `gen.writeNumberField(String, float)` +* `gen.writeObjectField(String, Object)` +* `gen.writeArrayFieldStart(String)` +* `gen.writeFieldName(String)` + +The first `String` parameter in the above methods refer to the names of the field (or key), while the second parameter (if present) refers to the value to be written. + +Note that `gen.writeFieldName(String)` only writes the field name, and not the value; similarly, `gen.writeArrayFieldStart(String)` only writes the field name and the array start marker. The value(s) will still have to be written if you choose either of these methods. + +There are other methods that can be more appropriate for your values (e.g. `gen.writeBooleanField(boolean)`); you may want to refer to the Jackson documentation for `JsonGenerator` for more information. + +After writing all the fields that are to be written, you can end the array or object with the appropriate marker: + +* `gen.writeEndObject()` +* `gen.writeEndArray()` + +With this in mind, a serializer for `Foo` instance might look something like this, if we go by the representation format we've decided earlier (and if we already have serializers for `Name` and `Tag`): + +```java + @Override + public void serialize(Foo val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartObject(); + + gen.writeNumberField("id", val.getId()); // int id + gen.writeObjectField("name", val.getName()); // Name name + gen.writeStringField("num", val.getNum().toString()); // BigDecimal num + gen.writeObjectField("tags", val.getTags()); // Set tags + + gen.writeEndObject(); + } +``` + +## Step 5: Implement the deserializer for the class + +Before we continue, you'll need a way to create new instances of `Foo`. In this case, we'll just use a `private` constructor, since we don't want other classes to create new `Foo` instances directly (e.g. if we require validation). And since the serdes classes are nested within the `Foo` class, this constructor will also be accessible to them. + +Now that we've gotten that out the way, we can continue onto implementing a deserializer. Naturally, it'll be more complex than implementing a serializer (as we have to take into account possible invalid inputs). + +Typically, when deserializing a JSON value into an object with serializers/deserializers defined, the `deserialize()` method of the deserializer for the class will be called with a `com.fasterxml.jackson.core.JsonParser` instance (and some other arguments). You'll have to interact with these instances to parse data from JSON values. + +There are many ways to deserialize a JSON value with a `JsonParser`, but we will only be focusing on some of them for the sake of brevity. + +First, we will want to read the JSON value as a `JsonNode` instance with `p.readValueAsTree()`. We may also want to store the *codec* for the parser, especially if we intend to delegate further deserialization to Jackson: + +```java + ObjectCodec codec = p.getCodec(); +``` + +Next, we'll have to handle this `JsonNode` instance. + +### 5a: Simple model classes + +For simple types (such as `Name`, which only has one `String` field), it's quite straightforward. + +We'll first need to read the node that our deserializer has to parse. The actual (runtime) type of this node will depend on the JSON value that we were given. + +There are many `JsonNode` subtypes available, including but not limited to: +* `TextNode` +* `IntNode` +* `ObjectNode` +* `ArrayNode` +* `NumberNode` + +Since we're expecting that JSON value to be a JSON string, our node should be an instance of `TextNode`. Performing an `instanceof` check here is good practice, as `JsonNode::textValue` returns `null` if the `JsonNode` is not a `TextNode`. It also allows us to return our own exception with a message that makes more sense. + +If it is, we'll get the value of that `TextNode`, perform any additional steps as needed (such as validation of values), then eventually create a `Name` instance with that value and return it. + +```java + @Override + public Name deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + + if (!(node instanceof TextNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + String name = node.textValue(); // not null as `node` is a `TextNode` + if (!Name.isValidName(name)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, Name.MESSAGE_CONSTRAINTS); + } + + return new Name(name); + } +``` + +Note that we have no need for an `ObjectCodec` instance here, because we aren't delegating the deserialization of this JSON string to Jackson again. + +In the next example, however, we'll need it: + +```java + JsonNode node = p.readValueAsTree(); + ObjectCodec codec = p.getCodec(); + + // extra validation so we throw an exception with our own message + // instead of some other exception with a less comprehensible message + if (!(node instanceof ArrayNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + List personList = node // is some sort of JsonNode + .traverse(codec) + .readValueAs(new TypeReference>(){}); + + UniquePersonList upl = new UniquePersonList(); + upl.setPersons(personList); + + return upl; +``` + +In the above example, we delegate the deserialization of the JSON array to the default deserializer for `List` instances. Note the use of `TypeReference`: this is required if we intend to deserialize an object to a generic type. For non-generic types, we can simply use: + +```java + // we don't really need to do explicit checks for `node` being an `ObjectNode` + // because we do that in the `Person` deserializer + // and we throw our own exception if the node we get isn't an `ObjectNode` + + Person person = node // is some sort of JsonNode + .traverse(codec) + .readValueAs(Person.class); +``` + +There are some cases in which delegation back to Jackson is not desirable. For instance, if a value is to be deserialized to a type not in the PersonSoft codebase, then a exception with a less relevant message may be thrown. Instead, we may want to recreate the instance manually: + +```java + // check `node instanceof TextNode`, etc... + + String durString = node.textValue(); // not null as `node` is a `TextNode` + Duration dur; + try { + dur = Duration.parse(decString); + } catch (DateTimeParseException e) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE, e); + } +``` + +This is also the case for the `List` above; however, we can generally expect that the JSON array elements are simply passed to our `Person` deserializer (regardless of JSON type), within which exceptions will be thrown. As a result, we only check that `node` is a JSON array. + +### 5b: Compound model classes + +For compound types (such as `Foo`, which have multiple fields and is serialized as a JSON object), the process is similar. + +We will first want to ensure that `node` (from `p.readValueAsTree()`) is an `ObjectNode`. We will then read the values associated with the keys we are interested in (as `JsonNode` instances as well), then handle them individually: + +```java + @Override + public Name deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + JsonCodec codec = p.getCodec(); + + if (!(node instanceof ObjectNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + ObjectNode objNode = (ObjectNode) node; + + // gets value associated with field "id", and makes sure that it exists and is an int value + // throws exception otherwise + int id = getNonNullNodeWithType(objNode, "id", ctx, IntNode.class).intValue(); + + // gets value associated with field "name", and makes sure that it exists (is not null) + // then attempts to deserialize it as a Name + // throws exception if anything fails + Name name = getNonNullNode(objNode, "name", ctx) + .traverse(codec) + .readValueAs(Name.class); + + // unlike Name and Tag, BigDecimal isn't part of our codebase, so... + // we create a BigDecimal instance manually here, and rethrow exceptions with our own message + // otherwise, it might throw an exception with a message that'll be possibly confusing for users + String decString = getNonNullNode(objNode, "num", ctx, TextNode.class) + BigDecimal dec; + try { + dec = new BigDecimal(decString); + } catch (NumberFormatException e) { + throw JsonUtil.getWrappedIllegalValueException(ctx, + INVALID_VAL_FMTR.apply("num"), // belongs to this deserializer class, see skeleton code + e); // cause of exception + } + + // first, make sure it's of a valid JSON type, i.e. an array + // then just deserialize it + Set tags = getNonNullNodeWithType(objNode, "tags", ctx, ArrayNode.class) + .traverse(codec) + .readValueAs(new TypeReference>(){}); + + // perform any additional validation here, if required + + return new Foo(id, name, dec, tags); + } +``` + +That's about it, really. + +Note that we make use of some new things: +* `getNonNullNode()`: a helper method (defined in the skeleton) which gets a value and ensures that it is not null (i.e. that it exists), +* `getNonNullNodeWithType()`: same as `getNonNullNode()`, but also checks that the `JsonNode` representing that value is also of the given type, and +* `INVALID_VAL_FMTR`: a `UnaryOperator` (i.e. function that takes a `String` and returns a `String`) used for formatting the message we use to indicate that the value of a certain field is malformed. This is also used in the `getNonNullNode*` methods. + +Other than these, the process is largely similar to that of [simple model classes](#5a-simple-model-classes). + +## Step 6: Write tests + +This is rather important -- writing serdes classes can often lead to many bugs, as you're effectively parsing arbitrary input. Writing tests for serializing and deserializing your new model classes can help you catch elementary mistakes, and ensure that the behavior stays just as expected even in edge cases. + +You can examine the `*SerdesTest.java` classes in `src/test/java/peoplesoft/model` for some ideas for test cases. Here are some to get you started: +* serialization results in expected JSON representation +* deserializing null value fails +* deserializing from an inappropriate JSON type fails (including arrays with mixed types, if relevant) +* deserializing with an invalid, malformed, or missing value fails (e.g. valid `String` but not a valid `BigDecimal` string) +* deserializing empty objects or arrays (if relevant) yields the expected behavior +* deserializing an invalid instance to a non-PeopleSoft class returns an exception with the expected message +* deserializing valid representation results in valid instance +* serializing an instance into a JSON representation and deserializing that JSON representation immediately results in a new instance that is effectively equal to the existing instance + +## Step 7: Integrate into other classes + +Now that you've gotten everything else out of the way, you can finally add your model class into other model classes as a field. You'll then likely also want to modify the serdes classes in the other model classes to handle this type. + +This is usually not difficult at all, especially for a field for something of type `Foo`. In this case, we can simply delegate the deserialization to Jackson again, which will select the appropriate serializer/deserializer to use. + +In any case, that should be all you need to do to write custom serializers and deserializers for model classes in the PeopleSoft code base. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 44e7c4d1d7b..6623300bebd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/seedu/address/AppParameters.java b/src/main/java/peoplesoft/AppParameters.java similarity index 93% rename from src/main/java/seedu/address/AppParameters.java rename to src/main/java/peoplesoft/AppParameters.java index ab552c398f3..d74eb1feec2 100644 --- a/src/main/java/seedu/address/AppParameters.java +++ b/src/main/java/peoplesoft/AppParameters.java @@ -1,4 +1,4 @@ -package seedu.address; +package peoplesoft; import java.nio.file.Path; import java.nio.file.Paths; @@ -7,8 +7,8 @@ import java.util.logging.Logger; import javafx.application.Application; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.FileUtil; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.commons.util.FileUtil; /** * Represents the parsed command-line parameters given to the application. diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/peoplesoft/Main.java similarity index 97% rename from src/main/java/seedu/address/Main.java rename to src/main/java/peoplesoft/Main.java index 052a5068631..5447ef11010 100644 --- a/src/main/java/seedu/address/Main.java +++ b/src/main/java/peoplesoft/Main.java @@ -1,4 +1,4 @@ -package seedu.address; +package peoplesoft; import javafx.application.Application; diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/peoplesoft/MainApp.java similarity index 79% rename from src/main/java/seedu/address/MainApp.java rename to src/main/java/peoplesoft/MainApp.java index 4133aaa0151..035a454263a 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/peoplesoft/MainApp.java @@ -1,4 +1,4 @@ -package seedu.address; +package peoplesoft; import java.io.IOException; import java.nio.file.Path; @@ -7,36 +7,36 @@ import javafx.application.Application; import javafx.stage.Stage; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.core.Version; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.ConfigUtil; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; -import seedu.address.logic.LogicManager; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; -import seedu.address.model.util.SampleDataUtil; -import seedu.address.storage.AddressBookStorage; -import seedu.address.storage.JsonAddressBookStorage; -import seedu.address.storage.JsonUserPrefsStorage; -import seedu.address.storage.Storage; -import seedu.address.storage.StorageManager; -import seedu.address.storage.UserPrefsStorage; -import seedu.address.ui.Ui; -import seedu.address.ui.UiManager; +import peoplesoft.commons.core.Config; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.commons.core.Version; +import peoplesoft.commons.exceptions.DataConversionException; +import peoplesoft.commons.util.ConfigUtil; +import peoplesoft.commons.util.StringUtil; +import peoplesoft.logic.Logic; +import peoplesoft.logic.LogicManager; +import peoplesoft.model.AddressBook; +import peoplesoft.model.Model; +import peoplesoft.model.ModelManager; +import peoplesoft.model.ReadOnlyAddressBook; +import peoplesoft.model.ReadOnlyUserPrefs; +import peoplesoft.model.UserPrefs; +import peoplesoft.model.util.SampleDataUtil; +import peoplesoft.storage.AddressBookStorage; +import peoplesoft.storage.JsonAddressBookStorage; +import peoplesoft.storage.JsonUserPrefsStorage; +import peoplesoft.storage.Storage; +import peoplesoft.storage.StorageManager; +import peoplesoft.storage.UserPrefsStorage; +import peoplesoft.ui.Ui; +import peoplesoft.ui.UiManager; /** * Runs the application. */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 0, true); + public static final Version VERSION = new Version(1, 3, 1, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -48,7 +48,7 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing PeopleSoft ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -65,7 +65,7 @@ public void init() throws Exception { logic = new LogicManager(model, storage); - ui = new UiManager(logic); + ui = new UiManager(logic, model); } /** @@ -79,14 +79,14 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { try { addressBookOptional = storage.readAddressBook(); 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 new sample data."); } initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); } catch (DataConversionException e) { - logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); + logger.warning("Data file not in the correct format. Will be starting with a new save file."); initialData = new AddressBook(); } 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 a new save file."); initialData = new AddressBook(); } @@ -167,13 +167,13 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting PeopleSoft" + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping PeopleSoft ] ============================="); try { storage.saveUserPrefs(model.getUserPrefs()); } catch (IOException e) { diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/peoplesoft/commons/core/Config.java similarity index 97% rename from src/main/java/seedu/address/commons/core/Config.java rename to src/main/java/peoplesoft/commons/core/Config.java index 91145745521..1d17267ed51 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/peoplesoft/commons/core/Config.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package peoplesoft.commons.core; import java.nio.file.Path; import java.nio.file.Paths; diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/peoplesoft/commons/core/GuiSettings.java similarity index 68% rename from src/main/java/seedu/address/commons/core/GuiSettings.java rename to src/main/java/peoplesoft/commons/core/GuiSettings.java index ba33653be67..4130b6e1fc7 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/peoplesoft/commons/core/GuiSettings.java @@ -1,17 +1,21 @@ -package seedu.address.commons.core; +package peoplesoft.commons.core; import java.awt.Point; -import java.io.Serializable; import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * A Serializable class that contains the GUI settings. * Guarantees: immutable. */ -public class GuiSettings implements Serializable { +public class GuiSettings { + public static final double MIN_WIDTH = 1250; + public static final double MIN_HEIGHT = 500; - private static final double DEFAULT_HEIGHT = 600; - private static final double DEFAULT_WIDTH = 740; + private static final double DEFAULT_WIDTH = 1280; + private static final double DEFAULT_HEIGHT = 720; private final double windowWidth; private final double windowHeight; @@ -29,9 +33,15 @@ public GuiSettings() { /** * Constructs a {@code GuiSettings} with the specified height, width and position. */ - public GuiSettings(double windowWidth, double windowHeight, int xPosition, int yPosition) { - this.windowWidth = windowWidth; - this.windowHeight = windowHeight; + @JsonCreator + public GuiSettings( + @JsonProperty("windowWidth") double windowWidth, + @JsonProperty("windowHeight") double windowHeight, + @JsonProperty("xPosition") int xPosition, + @JsonProperty("yPosition") int yPosition) { + // silently coerce loaded widths/heights to the minimum + this.windowWidth = Math.max(windowWidth, MIN_WIDTH); + this.windowHeight = Math.max(windowHeight, MIN_HEIGHT); windowCoordinates = new Point(xPosition, yPosition); } diff --git a/src/main/java/peoplesoft/commons/core/JobIdFactory.java b/src/main/java/peoplesoft/commons/core/JobIdFactory.java new file mode 100644 index 00000000000..1ee44810339 --- /dev/null +++ b/src/main/java/peoplesoft/commons/core/JobIdFactory.java @@ -0,0 +1,40 @@ +package peoplesoft.commons.core; + +import peoplesoft.model.util.ID; + +/** + * Class to generate unique {@code JobIds}. + */ +public class JobIdFactory { + private static int id = 0; + + /** + * Returns a unique {@code JobId}. + * + * @return JobId. + */ + public static ID nextId() { + return new ID(++id); + } + + /** + * Sets the current id. + * + * @param id To set. + */ + public static void setId(int id) { + if (id < 0) { + throw new IllegalArgumentException("id should not be negative"); + } + JobIdFactory.id = id; + } + + /** + * Returns the current id. + * + * @return Id. + */ + public static int getId() { + return id; + } +} diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/peoplesoft/commons/core/LogsCenter.java similarity index 99% rename from src/main/java/seedu/address/commons/core/LogsCenter.java rename to src/main/java/peoplesoft/commons/core/LogsCenter.java index 431e7185e76..5ede3b136de 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/peoplesoft/commons/core/LogsCenter.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package peoplesoft.commons.core; import java.io.IOException; import java.util.Arrays; diff --git a/src/main/java/peoplesoft/commons/core/Messages.java b/src/main/java/peoplesoft/commons/core/Messages.java new file mode 100644 index 00000000000..96fccaafd2c --- /dev/null +++ b/src/main/java/peoplesoft/commons/core/Messages.java @@ -0,0 +1,36 @@ +package peoplesoft.commons.core; + +/** + * Container for messages displayed to the user through the ResultDisplay. + */ +public class Messages { + + // -- General Commands -- + public static final String MSG_UNKNOWN_CMD = "Unknown command.\n" + + "Type \"help\" to see a list of available commands."; + public static final String MSG_INVALID_CMD_FORMAT = "Invalid command format. \n%1$s"; + public static final String MSG_EMPTY_STRING = "A field has been left empty.\nEnter the missing information."; + public static final String MSG_DURATION_CONSTRAINTS = "The duration should be a positive number (in hours)."; + public static final String MSG_DURATION_TOO_LARGE = "The input duration is too long.\n" + + "Consider adding the job as two jobs and splitting the duration between them."; + + // -- Person Commands -- + public static final String MSG_INVALID_PERSON_DISPLAYED_IDX = "Invalid index for person"; + public static final String MSG_PERSONS_LISTED_OVERVIEW = "%1$d persons listed."; + + // -- Job Commands -- + public static final String MSG_INVALID_JOB_DISPLAYED_IDX = "The specified job number " + + "is not in the displayed list. \nConsider using the \"list\" command to list all jobs."; + public static final String MSG_JOBS_LISTED_OVERVIEW = "%1$d jobs listed."; + public static final String MSG_MODIFY_FINAL_JOB = "Cannot modify a job that has been paid."; + public static final String MSG_ASSIGN_PERSON_TO_JOB = "Assign at least one person to this job."; + public static final String MSG_DUPLICATE_JOB = "This job already exists in the database"; + public static final String MSG_DUPLICATE_EMPLOYMENT = + "%s person(s) have already been assigned to this job.\n" + + "Assignments cannot be edited. Please delete this job and create a new one."; + public static final String MSG_ASSIGN_MARKED_JOB = "Unmark the job before assigning it."; + public static final String MSG_JOB_NOT_PAID_FAILURE = + "Payments cannot be finalized if the job is not marked as paid yet.\n" + + "Please refer to the user guide for the expected order of actions."; + +} diff --git a/src/main/java/peoplesoft/commons/core/PersonIdFactory.java b/src/main/java/peoplesoft/commons/core/PersonIdFactory.java new file mode 100644 index 00000000000..619ed6919ab --- /dev/null +++ b/src/main/java/peoplesoft/commons/core/PersonIdFactory.java @@ -0,0 +1,40 @@ +package peoplesoft.commons.core; + +import peoplesoft.model.util.ID; + +/** + * Class to generate unique person ids. + */ +public class PersonIdFactory { + private static int id = 0; + + /** + * Returns a unique person id. + * + * @return JobId. + */ + public static ID nextId() { + return new ID(++id); + } + + /** + * Sets the current id. + * + * @param id To set. + */ + public static void setId(int id) { + if (id < 0) { + throw new IllegalArgumentException("id should not be negative"); + } + PersonIdFactory.id = id; + } + + /** + * Returns the current id. + * + * @return Id. + */ + public static int getId() { + return id; + } +} diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/peoplesoft/commons/core/Version.java similarity index 98% rename from src/main/java/seedu/address/commons/core/Version.java rename to src/main/java/peoplesoft/commons/core/Version.java index 12142ec1e32..3512e9bc9c0 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/peoplesoft/commons/core/Version.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package peoplesoft.commons.core; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/peoplesoft/commons/core/index/Index.java similarity index 87% rename from src/main/java/seedu/address/commons/core/index/Index.java rename to src/main/java/peoplesoft/commons/core/index/Index.java index 19536439c09..14b626de756 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/peoplesoft/commons/core/index/Index.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core.index; +package peoplesoft.commons.core.index; /** * Represents a zero-based or one-based index. @@ -9,6 +9,8 @@ * convert it back to an int if the index will not be passed to a different component again. */ public class Index { + public static final String MESSAGE_CONSTRAINTS = "Index should be a non-zero positive integer."; + private int zeroBasedIndex; /** @@ -45,6 +47,11 @@ public static Index fromOneBased(int oneBasedIndex) { return new Index(oneBasedIndex - 1); } + @Override + public int hashCode() { + return Integer.hashCode(zeroBasedIndex); + } + @Override public boolean equals(Object other) { return other == this // short circuit if same object diff --git a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java b/src/main/java/peoplesoft/commons/exceptions/DataConversionException.java similarity index 84% rename from src/main/java/seedu/address/commons/exceptions/DataConversionException.java rename to src/main/java/peoplesoft/commons/exceptions/DataConversionException.java index 1f689bd8e3f..e0390a57363 100644 --- a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java +++ b/src/main/java/peoplesoft/commons/exceptions/DataConversionException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package peoplesoft.commons.exceptions; /** * Represents an error during conversion of data from one format to another diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java b/src/main/java/peoplesoft/commons/exceptions/IllegalValueException.java similarity index 93% rename from src/main/java/seedu/address/commons/exceptions/IllegalValueException.java rename to src/main/java/peoplesoft/commons/exceptions/IllegalValueException.java index 19124db485c..d6adef9d3fd 100644 --- a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java +++ b/src/main/java/peoplesoft/commons/exceptions/IllegalValueException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package peoplesoft.commons.exceptions; /** * Signals that some given data does not fulfill some constraints. diff --git a/src/main/java/seedu/address/commons/util/AppUtil.java b/src/main/java/peoplesoft/commons/util/AppUtil.java similarity index 94% rename from src/main/java/seedu/address/commons/util/AppUtil.java rename to src/main/java/peoplesoft/commons/util/AppUtil.java index 87aa89c0326..f49a1c37df5 100644 --- a/src/main/java/seedu/address/commons/util/AppUtil.java +++ b/src/main/java/peoplesoft/commons/util/AppUtil.java @@ -1,9 +1,9 @@ -package seedu.address.commons.util; +package peoplesoft.commons.util; import static java.util.Objects.requireNonNull; import javafx.scene.image.Image; -import seedu.address.MainApp; +import peoplesoft.MainApp; /** * A container for App specific utility functions diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/peoplesoft/commons/util/CollectionUtil.java similarity index 96% rename from src/main/java/seedu/address/commons/util/CollectionUtil.java rename to src/main/java/peoplesoft/commons/util/CollectionUtil.java index eafe4dfd681..a98e871ecc9 100644 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ b/src/main/java/peoplesoft/commons/util/CollectionUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package peoplesoft.commons.util; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/commons/util/ConfigUtil.java b/src/main/java/peoplesoft/commons/util/ConfigUtil.java similarity index 77% rename from src/main/java/seedu/address/commons/util/ConfigUtil.java rename to src/main/java/peoplesoft/commons/util/ConfigUtil.java index f7f8a2bd44c..8c945d93ad0 100644 --- a/src/main/java/seedu/address/commons/util/ConfigUtil.java +++ b/src/main/java/peoplesoft/commons/util/ConfigUtil.java @@ -1,11 +1,11 @@ -package seedu.address.commons.util; +package peoplesoft.commons.util; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.core.Config; -import seedu.address.commons.exceptions.DataConversionException; +import peoplesoft.commons.core.Config; +import peoplesoft.commons.exceptions.DataConversionException; /** * A class for accessing the Config File. diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/peoplesoft/commons/util/FileUtil.java similarity index 98% rename from src/main/java/seedu/address/commons/util/FileUtil.java rename to src/main/java/peoplesoft/commons/util/FileUtil.java index b1e2767cdd9..03d32f14963 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/peoplesoft/commons/util/FileUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package peoplesoft.commons.util; import java.io.IOException; import java.nio.file.Files; diff --git a/src/main/java/peoplesoft/commons/util/JsonUtil.java b/src/main/java/peoplesoft/commons/util/JsonUtil.java new file mode 100644 index 00000000000..428ebf8c812 --- /dev/null +++ b/src/main/java/peoplesoft/commons/util/JsonUtil.java @@ -0,0 +1,267 @@ +package peoplesoft.commons.util; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.UnaryOperator; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; + +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.commons.exceptions.DataConversionException; +import peoplesoft.commons.exceptions.IllegalValueException; + +/** + * Converts a Java object instance to JSON and vice versa + */ +public class JsonUtil { + + private static final Logger logger = LogsCenter.getLogger(JsonUtil.class); + + private static ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules() + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .registerModule(new SimpleModule("SimpleModule") + .addSerializer(Level.class, new ToStringSerializer()) + .addDeserializer(Level.class, new LevelDeserializer(Level.class))); + + static void serializeObjectToJsonFile(Path jsonFile, T objectToSerialize) throws IOException { + FileUtil.writeToFile(jsonFile, toJsonString(objectToSerialize)); + } + + static T deserializeObjectFromJsonFile(Path jsonFile, Class classOfObjectToDeserialize) + throws IOException, JsonMappingException { + return fromJsonString(FileUtil.readFromFile(jsonFile), classOfObjectToDeserialize); + } + + /** + * Returns the Json object from the given file or {@code Optional.empty()} object if the file is not found. + * If any values are missing from the file, default values will be used, as long as the file is a valid json file. + * @param filePath cannot be null. + * @param classOfObjectToDeserialize Json file has to correspond to the structure in the class given here. + * @throws DataConversionException if the file format is not as expected. + */ + public static Optional readJsonFile( + Path filePath, Class classOfObjectToDeserialize) throws DataConversionException { + requireNonNull(filePath); + + if (!Files.exists(filePath)) { + logger.info("Json file " + filePath + " not found"); + return Optional.empty(); + } + + T jsonFile; + + try { + jsonFile = deserializeObjectFromJsonFile(filePath, classOfObjectToDeserialize); + } catch (JsonMappingException e) { + logger.info("Illegal values found in " + filePath + ": " + e.getMessage()); + throw new DataConversionException(e); + } catch (IOException e) { + logger.warning("Error reading from jsonFile file " + filePath + ": " + e); + throw new DataConversionException(e); + } + + return Optional.of(jsonFile); + } + + /** + * Saves the Json object to the specified file. + * Overwrites existing file if it exists, creates a new file if it doesn't. + * @param jsonFile cannot be null + * @param filePath cannot be null + * @throws IOException if there was an error during writing to the file + */ + public static void saveJsonFile(T jsonFile, Path filePath) throws IOException { + requireNonNull(filePath); + requireNonNull(jsonFile); + + serializeObjectToJsonFile(filePath, jsonFile); + } + + + /** + * Converts a given string representation of a JSON data to instance of a class + * @param The generic type to create an instance of + * @return The instance of T with the specified values in the JSON string + */ + public static T fromJsonString(String json, Class instanceClass) throws IOException, JsonMappingException { + return objectMapper.readValue(json, instanceClass); + } + + /** + * Converts a given instance of a class into its JSON data string representation + * @param instance The T object to be converted into the JSON string + * @param The generic type to create an instance of + * @return JSON data representation of the given class instance, in string + */ + public static String toJsonString(T instance) throws JsonProcessingException { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(instance); + } + + /** + * Creates a {@code JsonMappingException} instance that wraps an {@code IllegalValueException} using the + * given context and message. + * + * @param ctx the {@code SerializerProvider} context (from a {@code Serializer}) + * @param msg the message for the {@code JsonMappingException} and {@code IllegalValueException} + * @param cause the cause for the {@code IllegalValueException} + * @return a {@code JsonMappingException} that wraps an {@code IllegalValueException} + */ + public static JsonMappingException getWrappedIllegalValueException(SerializerProvider ctx, String msg, + Throwable cause) { + IllegalValueException ive = new IllegalValueException(msg, cause); + return JsonMappingException.from(ctx, msg, ive); + } + + /** + * Creates a {@code JsonMappingException} instance that wraps an {@code IllegalValueException} using the + * given context and message. + * + * @param ctx the {@code SerializerProvider} context (from a {@code Serializer}) + * @param msg the message for the {@code JsonMappingException} and {@code IllegalValueException} + * @return a {@code JsonMappingException} that wraps an {@code IllegalValueException} + */ + public static JsonMappingException getWrappedIllegalValueException(SerializerProvider ctx, String msg) { + IllegalValueException ive = new IllegalValueException(msg); + return JsonMappingException.from(ctx, msg, ive); + } + + /** + * Creates a {@code JsonMappingException} instance that wraps an {@code IllegalValueException} using the + * given context and message. + * + * @param ctx the {@code DeserializationContext} (from a {@code Deerializer}) + * @param msg the message for the {@code JsonMappingException} and {@code IllegalValueException} + * @return a {@code JsonMappingException} that wraps an {@code IllegalValueException} + */ + public static JsonMappingException getWrappedIllegalValueException(DeserializationContext ctx, String msg) { + IllegalValueException ive = new IllegalValueException(msg); + return JsonMappingException.from(ctx, msg, ive); + } + + /** + * Creates a {@code JsonMappingException} instance that wraps an {@code IllegalValueException} using the + * given context and message. + * + * @param ctx the {@code DeserializationContext} (from a {@code Deerializer}) + * @param msg the message for the {@code JsonMappingException} and {@code IllegalValueException} + * @param cause the cause for the {@code IllegalValueException} + * @return a {@code JsonMappingException} that wraps an {@code IllegalValueException} + */ + public static JsonMappingException getWrappedIllegalValueException(DeserializationContext ctx, String msg, + Throwable cause) { + IllegalValueException ive = new IllegalValueException(msg, cause); + return JsonMappingException.from(ctx, msg, ive); + } + + /** + * Gets the (non-null) {@code JsonNode} representing the value stored at the given key in the + * {@code ObjectNode}. + * + * If there is no such node at the given key (i.e. the {@code JsonNode} is {@code null}), then a + * {@code JsonMappingException} will be thrown. + * + * @param node the object to retrieve the value from + * @param key the key of the value + * @param ctx the current deserialization context + * @param errMsgFormatter a unary operator that takes the key as an argument, and returns a string + * @return the {@code JsonNode} representing the value stored at the given key in the given object + * @throws JsonMappingException if there is no such key in the given object + */ + public static JsonNode getNonNullNode(ObjectNode node, String key, DeserializationContext ctx, + UnaryOperator errMsgFormatter) throws JsonMappingException { + JsonNode jsonNode = node.get(key); + if (jsonNode == null) { + throw JsonUtil.getWrappedIllegalValueException( + ctx, errMsgFormatter.apply(key)); + } + + return jsonNode; + } + + /** + * Gets the (non-null) {@code JsonNode} of type {@code T} representing the value stored at the given key + * in the {@code ObjectNode}. + * + * Generally, the only meaningful types for {@code T} are subclasses of {@code JsonNode}, including but + * not limited to {@code ObjectNode}, {@code TextNode}, and {@code IntNode}. + * + * If there is no such node at the given key (i.e. the {@code JsonNode} is {@code null}), then a + * {@code JsonMappingException} will be thrown. + * + * If the type of the node does not match {@code cls}, then a {@code JsonMappingException} will also be + * thrown. + * + * @param the type of {@code JsonNode} to be returned + * @param node the object to retrieve the value from + * @param key the key of the value + * @param ctx the current deserialization context + * @param errMsgFormatter a unary operator that takes the key as an argument, and returns a string + * @param cls the type of {@code JsonNode} to be returned + * @return the {@code JsonNode} representing the value stored at the given key in the given object + * @throws JsonMappingException if there is no such key in the given object, or the type of the + * {@code JsonNode} does not match {@code cls} + */ + public static T getNonNullNodeWithType(ObjectNode node, String key, DeserializationContext ctx, + UnaryOperator errMsgFormatter, Class cls) throws JsonMappingException { + JsonNode jsonNode = getNonNullNode(node, key, ctx, errMsgFormatter); + if (!cls.isInstance(jsonNode)) { + throw JsonUtil.getWrappedIllegalValueException( + ctx, errMsgFormatter.apply(key)); + } + + return cls.cast(jsonNode); + } + + /** + * Contains methods that retrieve logging level from serialized string. + */ + private static class LevelDeserializer extends FromStringDeserializer { + + protected LevelDeserializer(Class vc) { + super(vc); + } + + @Override + protected Level _deserialize(String value, DeserializationContext ctxt) { + return getLoggingLevel(value); + } + + /** + * Gets the logging level that matches loggingLevelString + *

+ * Returns null if there are no matches + * + */ + private Level getLoggingLevel(String loggingLevelString) { + return Level.parse(loggingLevelString); + } + + @Override + public Class handledType() { + return Level.class; + } + } + +} diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/peoplesoft/commons/util/StringUtil.java similarity index 95% rename from src/main/java/seedu/address/commons/util/StringUtil.java rename to src/main/java/peoplesoft/commons/util/StringUtil.java index 61cc8c9a1cb..f8a84d474f5 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/peoplesoft/commons/util/StringUtil.java @@ -1,7 +1,7 @@ -package seedu.address.commons.util; +package peoplesoft.commons.util; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static peoplesoft.commons.util.AppUtil.checkArgument; import java.io.PrintWriter; import java.io.StringWriter; diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/peoplesoft/logic/Logic.java similarity index 59% rename from src/main/java/seedu/address/logic/Logic.java rename to src/main/java/peoplesoft/logic/Logic.java index 92cd8fa605a..ddca3649093 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/peoplesoft/logic/Logic.java @@ -1,14 +1,16 @@ -package seedu.address.logic; +package peoplesoft.logic; import java.nio.file.Path; import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; +import peoplesoft.commons.core.GuiSettings; +import peoplesoft.logic.commands.CommandHelpMessage; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.ReadOnlyAddressBook; +import peoplesoft.model.job.Job; +import peoplesoft.model.person.Person; /** * API of the Logic component @@ -26,13 +28,19 @@ public interface Logic { /** * Returns the AddressBook. * - * @see seedu.address.model.Model#getAddressBook() + * @see peoplesoft.model.Model#getAddressBook() */ ReadOnlyAddressBook getAddressBook(); /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of persons */ + ObservableList getFilteredJobList(); + + /** Returns an unmodifiable view of the list of command help messages */ + ObservableList getCommandHelpMessageList(); + /** * Returns the user prefs' address book file path. */ diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/peoplesoft/logic/LogicManager.java similarity index 68% rename from src/main/java/seedu/address/logic/LogicManager.java rename to src/main/java/peoplesoft/logic/LogicManager.java index 9d9c6d15bdc..cb67919e0e0 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/peoplesoft/logic/LogicManager.java @@ -1,21 +1,24 @@ -package seedu.address.logic; +package peoplesoft.logic; import java.io.IOException; import java.nio.file.Path; import java.util.logging.Logger; import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.Model; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; -import seedu.address.storage.Storage; +import peoplesoft.commons.core.GuiSettings; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandHelpMessage; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.HelpCommand; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.logic.parser.AddressBookParser; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.Model; +import peoplesoft.model.ReadOnlyAddressBook; +import peoplesoft.model.job.Job; +import peoplesoft.model.person.Person; +import peoplesoft.storage.Storage; /** * The main LogicManager of the app. @@ -64,6 +67,16 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getFilteredJobList() { + return model.getFilteredJobList(); + } + + @Override + public ObservableList getCommandHelpMessageList() { + return HelpCommand.COMMANDS; + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/peoplesoft/logic/commands/ClearCommand.java b/src/main/java/peoplesoft/logic/commands/ClearCommand.java new file mode 100644 index 00000000000..df399f5ff39 --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/ClearCommand.java @@ -0,0 +1,31 @@ +package peoplesoft.logic.commands; + +import static java.util.Objects.requireNonNull; + +import peoplesoft.commons.core.JobIdFactory; +import peoplesoft.commons.core.PersonIdFactory; +import peoplesoft.model.AddressBook; +import peoplesoft.model.Model; +import peoplesoft.model.employment.Employment; + +/** + * Clears the address book. + */ +public class ClearCommand extends Command { + + public static final String COMMAND_WORD = "clear"; + public static final String COMMAND_EXAMPLES = "N.A."; + public static final String COMMAND_FORMAT = COMMAND_WORD; + public static final String MESSAGE_SUCCESS = "Employee list has been cleared."; + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.setAddressBook(new AddressBook()); + // Resets association and jobId + Employment.newInstance(); + JobIdFactory.setId(0); + PersonIdFactory.setId(0); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/peoplesoft/logic/commands/Command.java similarity index 78% rename from src/main/java/seedu/address/logic/commands/Command.java rename to src/main/java/peoplesoft/logic/commands/Command.java index 64f18992160..f61e60bf8d5 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/peoplesoft/logic/commands/Command.java @@ -1,7 +1,7 @@ -package seedu.address.logic.commands; +package peoplesoft.logic.commands; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.model.Model; /** * Represents a command with hidden internal logic and the ability to be executed. diff --git a/src/main/java/peoplesoft/logic/commands/CommandHelpMessage.java b/src/main/java/peoplesoft/logic/commands/CommandHelpMessage.java new file mode 100644 index 00000000000..51d37d721d0 --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/CommandHelpMessage.java @@ -0,0 +1,51 @@ +package peoplesoft.logic.commands; + +import javafx.beans.property.SimpleStringProperty; + +/** + * Handle help instructions for commands. + */ +public class CommandHelpMessage { + public static final String DELIMITER = " "; + + private final SimpleStringProperty command; + private final SimpleStringProperty format; + private final SimpleStringProperty examples; + + + /** + * Constructor for CommandHelpMessage. + * + * @param command command word + * @param format format descriptor of command + * @param examples example usage of command + */ + public CommandHelpMessage(String command, String format, String examples) { + this.command = new SimpleStringProperty(command); + this.format = new SimpleStringProperty(format); + this.examples = new SimpleStringProperty(examples); + } + + public String getCommand() { + return command.get(); + } + + public String getFormat() { + return format.get(); + } + + public String getExamples() { + return examples.get(); + } + + /** + * Converts a command's help message into string delimited by the property delimiter. + * + * @return formatted help message. + */ + public String toString() { + return getCommand() + DELIMITER + + getFormat() + DELIMITER + + getExamples(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/peoplesoft/logic/commands/CommandResult.java similarity index 97% rename from src/main/java/seedu/address/logic/commands/CommandResult.java rename to src/main/java/peoplesoft/logic/commands/CommandResult.java index 92f900b7916..2a82297efa2 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/peoplesoft/logic/commands/CommandResult.java @@ -1,4 +1,4 @@ -package seedu.address.logic.commands; +package peoplesoft.logic.commands; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/peoplesoft/logic/commands/ExitCommand.java similarity index 61% rename from src/main/java/seedu/address/logic/commands/ExitCommand.java rename to src/main/java/peoplesoft/logic/commands/ExitCommand.java index 3dd85a8ba90..754e1927ee1 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/peoplesoft/logic/commands/ExitCommand.java @@ -1,6 +1,6 @@ -package seedu.address.logic.commands; +package peoplesoft.logic.commands; -import seedu.address.model.Model; +import peoplesoft.model.Model; /** * Terminates the program. @@ -8,8 +8,10 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; + public static final String COMMAND_EXAMPLES = "N.A."; + public static final String COMMAND_FORMAT = COMMAND_WORD; - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting PeopleSoft as requested..."; @Override public CommandResult execute(Model model) { diff --git a/src/main/java/peoplesoft/logic/commands/ExportCommand.java b/src/main/java/peoplesoft/logic/commands/ExportCommand.java new file mode 100644 index 00000000000..a614aa0e4da --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/ExportCommand.java @@ -0,0 +1,70 @@ +package peoplesoft.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.util.List; + +import peoplesoft.commons.core.Messages; +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.logic.export.Exporter; +import peoplesoft.model.Model; +import peoplesoft.model.person.Person; + + +/** + * Exports a person identified using it's displayed index from the database. + */ +public class ExportCommand extends Command { + + public static final String COMMAND_WORD = "export"; + public static final String COMMAND_EXAMPLES = COMMAND_WORD + " 1"; + public static final String COMMAND_FORMAT = COMMAND_WORD + " INDEX"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Exports a .csv file with the jobs the person worked on, " + + "including how much pay they should expect to receive.\n" + + "All the jobs the person worked on will be displayed under the Jobs list.\n" + + "Format: " + COMMAND_WORD + " INDEX. \n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_EXPORT_PERSON_SUCCESS = "%s's details were " + + "exported to a .CSV in your data folder using their name. \n" + + "Now displaying the jobs that they were assigned to.\n" + + "Use the \"list\" command to see all jobs again."; + + public static final String MESSAGE_EXPORT_PERSON_FAILURE = "Failed to export " + + "due to a problem with saving."; + + private final Index targetIndex; + + public ExportCommand(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.MSG_INVALID_PERSON_DISPLAYED_IDX); + } + + Person personToExport = lastShownList.get(targetIndex.getZeroBased()); + try { + Exporter.getNewInstance(personToExport, model).export(); + return new CommandResult(String.format(MESSAGE_EXPORT_PERSON_SUCCESS, personToExport.getName())); + } catch (IOException ioException) { + return new CommandResult(String.format(MESSAGE_EXPORT_PERSON_FAILURE)); + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ExportCommand // instanceof handles nulls + && targetIndex.equals(((ExportCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/peoplesoft/logic/commands/HelpCommand.java b/src/main/java/peoplesoft/logic/commands/HelpCommand.java new file mode 100644 index 00000000000..8cf91b3d58d --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/HelpCommand.java @@ -0,0 +1,125 @@ +package peoplesoft.logic.commands; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import peoplesoft.logic.commands.job.JobAddCommand; +import peoplesoft.logic.commands.job.JobAssignCommand; +import peoplesoft.logic.commands.job.JobDeleteCommand; +import peoplesoft.logic.commands.job.JobFinalizeCommand; +import peoplesoft.logic.commands.job.JobFindCommand; +import peoplesoft.logic.commands.job.JobListCommand; +import peoplesoft.logic.commands.job.JobMarkCommand; +import peoplesoft.logic.commands.person.PersonAddCommand; +import peoplesoft.logic.commands.person.PersonDeleteCommand; +import peoplesoft.logic.commands.person.PersonEditCommand; +import peoplesoft.logic.commands.person.PersonFindCommand; +import peoplesoft.logic.commands.person.PersonListCommand; +import peoplesoft.model.Model; + +/** + * Format full help instructions for every command for display. + */ +public class HelpCommand extends Command { + + public static final String COMMAND_WORD = "help"; + public static final String COMMAND_EXAMPLES = "N.A."; + public static final String COMMAND_FORMAT = COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" + + "Example: " + COMMAND_WORD; + + public static final String SHOWING_HELP_MESSAGE = "Opened the help page.\n" + + "Type any other command to return to the overview page."; + + public static final CommandHelpMessage JOB_ADD_COMMAND = new CommandHelpMessage( + JobAddCommand.COMMAND_WORD, + JobAddCommand.COMMAND_FORMAT, + JobAddCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage JOB_ASSIGN_COMMAND = new CommandHelpMessage( + JobAssignCommand.COMMAND_WORD, + JobAssignCommand.COMMAND_FORMAT, + JobAssignCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage JOB_DELETE_COMMAND = new CommandHelpMessage( + JobDeleteCommand.COMMAND_WORD, + JobDeleteCommand.COMMAND_FORMAT, + JobDeleteCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage JOB_FINALIZE_COMMAND = new CommandHelpMessage( + JobFinalizeCommand.COMMAND_WORD, + JobFinalizeCommand.COMMAND_FORMAT, + JobFinalizeCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage JOB_FIND_COMMAND = new CommandHelpMessage( + JobFindCommand.COMMAND_WORD, + JobFindCommand.COMMAND_FORMAT, + JobFindCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage JOB_LIST_COMMAND = new CommandHelpMessage( + JobListCommand.COMMAND_WORD, + JobListCommand.COMMAND_FORMAT, + JobListCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage JOB_MARK_COMMAND = new CommandHelpMessage( + JobMarkCommand.COMMAND_WORD, + JobMarkCommand.COMMAND_FORMAT, + JobMarkCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage PERSON_ADD_COMMAND = new CommandHelpMessage( + PersonAddCommand.COMMAND_WORD, + PersonAddCommand.COMMAND_FORMAT, + PersonAddCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage PERSON_DELETE_COMMAND = new CommandHelpMessage( + PersonDeleteCommand.COMMAND_WORD, + PersonDeleteCommand.COMMAND_FORMAT, + PersonDeleteCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage PERSON_EDIT_COMMAND = new CommandHelpMessage( + PersonEditCommand.COMMAND_WORD, + PersonEditCommand.COMMAND_FORMAT, + PersonEditCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage PERSON_FIND_COMMAND = new CommandHelpMessage( + PersonFindCommand.COMMAND_WORD, + PersonFindCommand.COMMAND_FORMAT, + PersonFindCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage PERSON_LIST_COMMAND = new CommandHelpMessage( + PersonListCommand.COMMAND_WORD, + PersonListCommand.COMMAND_FORMAT, + PersonListCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage CLEAR_COMMAND = new CommandHelpMessage( + ClearCommand.COMMAND_WORD, + ClearCommand.COMMAND_FORMAT, + ClearCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage EXIT_COMMAND = new CommandHelpMessage( + ExitCommand.COMMAND_WORD, + ExitCommand.COMMAND_FORMAT, + ExitCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage EXPORT_COMMAND = new CommandHelpMessage( + ExportCommand.COMMAND_WORD, + ExportCommand.COMMAND_FORMAT, + ExportCommand.COMMAND_EXAMPLES); + + public static final CommandHelpMessage HELP_COMMAND = new CommandHelpMessage( + COMMAND_WORD, + COMMAND_FORMAT, + COMMAND_EXAMPLES); + + public static final ObservableList COMMANDS = + FXCollections.observableArrayList( + PERSON_ADD_COMMAND, PERSON_EDIT_COMMAND, PERSON_DELETE_COMMAND, PERSON_FIND_COMMAND, + PERSON_LIST_COMMAND, JOB_ADD_COMMAND, JOB_ASSIGN_COMMAND, JOB_DELETE_COMMAND, + JOB_FIND_COMMAND, JOB_LIST_COMMAND, JOB_MARK_COMMAND, JOB_FINALIZE_COMMAND, + CLEAR_COMMAND, EXIT_COMMAND, EXPORT_COMMAND, HELP_COMMAND); + + @Override + public CommandResult execute(Model model) { + return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + } +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/peoplesoft/logic/commands/exceptions/CommandException.java similarity index 89% rename from src/main/java/seedu/address/logic/commands/exceptions/CommandException.java rename to src/main/java/peoplesoft/logic/commands/exceptions/CommandException.java index a16bd14f2cd..8d9fb5dbded 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/peoplesoft/logic/commands/exceptions/CommandException.java @@ -1,4 +1,4 @@ -package seedu.address.logic.commands.exceptions; +package peoplesoft.logic.commands.exceptions; /** * Represents an error which occurs during execution of a {@link Command}. diff --git a/src/main/java/peoplesoft/logic/commands/job/JobAddCommand.java b/src/main/java/peoplesoft/logic/commands/job/JobAddCommand.java new file mode 100644 index 00000000000..680e224286a --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/job/JobAddCommand.java @@ -0,0 +1,75 @@ +package peoplesoft.logic.commands.job; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_DURATION; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_NAME; + +import peoplesoft.commons.core.Messages; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.Model; +import peoplesoft.model.job.Job; + +/** + * Adds a {@code Job} to {@code AddressBook}. + */ +public class JobAddCommand extends Command { + + public static final String COMMAND_WORD = "add"; + + public static final String COMMAND_EXAMPLES = COMMAND_WORD + " " + + PREFIX_NAME + "Fix HDB Lock " + + PREFIX_DURATION + "1"; + + public static final String COMMAND_FORMAT = COMMAND_WORD + " " + + PREFIX_NAME + "NAME " + + PREFIX_DURATION + "DURATION "; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a job to the database.\n" + + "Format: " + + COMMAND_WORD + " " + + PREFIX_NAME + "NAME " + + PREFIX_DURATION + "DURATION\n" + + "Example: " + + COMMAND_WORD + " " + + PREFIX_NAME + "Fix washing machine " + + PREFIX_DURATION + "2.5\n"; + + public static final String MESSAGE_SUCCESS = "New job added: %s"; + + private final Job toAdd; + + /** + * Creates a {@code JobAddCommand} to add a {@code Job}. + * + * @param toAdd Job. + * @throws ParseException Thrown if there is an error with parsing. + */ + public JobAddCommand(Job toAdd) { + requireNonNull(toAdd); + this.toAdd = toAdd; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasJob(toAdd.getJobId())) { // Note: user will never trigger this error + throw new CommandException(Messages.MSG_DUPLICATE_JOB); + } + + model.addJob(toAdd); + model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof JobAddCommand // instanceof handles nulls + && toAdd.equals(((JobAddCommand) other).toAdd)); + } +} diff --git a/src/main/java/peoplesoft/logic/commands/job/JobAssignCommand.java b/src/main/java/peoplesoft/logic/commands/job/JobAssignCommand.java new file mode 100644 index 00000000000..4f0fcadabc7 --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/job/JobAssignCommand.java @@ -0,0 +1,147 @@ +package peoplesoft.logic.commands.job; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_INDEX; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import peoplesoft.commons.core.Messages; +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.model.Model; +import peoplesoft.model.employment.Employment; +import peoplesoft.model.employment.exceptions.DuplicateEmploymentException; +import peoplesoft.model.job.Job; +import peoplesoft.model.person.Person; + +/** + * Assigns a {@code Job} to a {@code Person}. + */ +public class JobAssignCommand extends Command { + + public static final String COMMAND_WORD = "assign"; + + public static final String COMMAND_EXAMPLES = COMMAND_WORD + " " + + "2 " + + PREFIX_INDEX + "1"; + + public static final String COMMAND_FORMAT = COMMAND_WORD + " " + + "JOB_INDEX " + + PREFIX_INDEX + "PERSON_INDEX [i/PERSON_INDEX]..."; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Assigns a job to one or more person(s).\n" + + "Format: " + COMMAND_WORD + " JOB_INDEX " + + PREFIX_INDEX + "PERSON_INDEX [PERSON_INDEX]... (as many persons as needed)\n" + + "Example: " + COMMAND_WORD + " 2 " + + PREFIX_INDEX + "3 " + PREFIX_INDEX + "4"; + + public static final String MESSAGE_SUCCESS = "Assigned job \"%s\" to %s."; + + private Index jobIndex; + private Set personIndexes; + private Employment instance; + + /** + * Creates a {@code JobAssignCommand} to assign a {@code Job} to a set of {@code Person}s. + * + * @param jobIndex Index of the job. + * @param personIndexes Set of indexes of persons. + * @param employment Employment to use (for easier testing). + */ + public JobAssignCommand(Index jobIndex, Set personIndexes, Employment employment) { + requireAllNonNull(jobIndex, personIndexes, employment); + this.jobIndex = jobIndex; + this.personIndexes = personIndexes; + this.instance = employment; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownJobs = model.getFilteredJobList(); + List lastShownPersons = model.getFilteredPersonList(); + + if (jobIndex.getZeroBased() >= lastShownJobs.size()) { + throw new CommandException(Messages.MSG_INVALID_JOB_DISPLAYED_IDX); + } + + Job job = lastShownJobs.get(jobIndex.getZeroBased()); + + if (job.isFinal()) { + throw new CommandException(Messages.MSG_MODIFY_FINAL_JOB); + } + + if (job.hasPaid()) { + throw new CommandException(Messages.MSG_ASSIGN_MARKED_JOB); + } + + for (Index i : personIndexes) { + if (i.getZeroBased() >= lastShownPersons.size()) { + throw new CommandException(Messages.MSG_INVALID_PERSON_DISPLAYED_IDX); + } + } + + Set persons = getPersons(personIndexes, lastShownPersons); + + int dupeCount = 0; + StringBuilder stringPersons = new StringBuilder(); + + for (Person p : persons) { + try { + instance.associate(job, p); + stringPersons.append(p.getName()).append(", "); + } catch (DuplicateEmploymentException e) { + dupeCount++; + } + } + if (stringPersons.length() > 0) { + stringPersons.delete(stringPersons.length() - 2, stringPersons.length()); + } + String successPersons = stringPersons.toString(); + + if (dupeCount > 0) { + if (successPersons.isBlank()) { + throw new CommandException(String.format(Messages.MSG_DUPLICATE_EMPLOYMENT, dupeCount)); + } else { + throw new CommandException(String.format(Messages.MSG_DUPLICATE_EMPLOYMENT, dupeCount) + "\n" + + String.format(MESSAGE_SUCCESS, job.getDesc(), successPersons)); + } + } + + // show the new assignement + model.updateFilteredJobList(Model.PREDICATE_SHOW_ALL_JOBS); + + return new CommandResult(String.format(MESSAGE_SUCCESS, job.getDesc(), successPersons)); + } + + private Set getPersons(Set personIndexes, List personList) { + Set persons = new HashSet<>(); + for (Index i : personIndexes) { + persons.add(personList.get(i.getZeroBased())); + } + return persons; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof JobAssignCommand)) { + return false; + } + + // state check + JobAssignCommand c = (JobAssignCommand) other; + return jobIndex.equals(c.jobIndex) + && personIndexes.equals(c.personIndexes); + } +} diff --git a/src/main/java/peoplesoft/logic/commands/job/JobDeleteCommand.java b/src/main/java/peoplesoft/logic/commands/job/JobDeleteCommand.java new file mode 100644 index 00000000000..30ffb54b259 --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/job/JobDeleteCommand.java @@ -0,0 +1,74 @@ +package peoplesoft.logic.commands.job; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import peoplesoft.commons.core.Messages; +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.model.Model; +import peoplesoft.model.employment.Employment; +import peoplesoft.model.job.Job; + +/** + * Deletes a {@code Job} with a given {@code JobId}. + */ +public class JobDeleteCommand extends Command { + + public static final String COMMAND_WORD = "delete"; + + public static final String COMMAND_EXAMPLES = COMMAND_WORD + " 3"; + + public static final String COMMAND_FORMAT = COMMAND_WORD + " JOB_INDEX"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the job identified by the index.\n" + + "Format: " + COMMAND_WORD + " INDEX\n" + + "Example: " + COMMAND_WORD + " 3"; + + public static final String MESSAGE_SUCCESS = "\"%s\" has been deleted."; + + private final Index toDelete; + + /** + * Creates a {@code JobDeleteCommand} to delete a {@code Job} by {@code Index}. + * + * @param toDelete Index of job to delete. + */ + public JobDeleteCommand(Index toDelete) { + requireNonNull(toDelete); + this.toDelete = toDelete; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredJobList(); + + if (toDelete.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MSG_INVALID_JOB_DISPLAYED_IDX); + } + + Job jobToDelete = lastShownList.get(toDelete.getZeroBased()); + + if (jobToDelete.isFinal()) { + throw new CommandException(Messages.MSG_MODIFY_FINAL_JOB); + } + + model.deleteJob(jobToDelete); + // Deletes employment associations + Employment.getInstance().deleteJob(jobToDelete); + + return new CommandResult(String.format(MESSAGE_SUCCESS, jobToDelete.getDesc())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof JobDeleteCommand // instanceof handles nulls + && toDelete.equals(((JobDeleteCommand) other).toDelete)); + } +} diff --git a/src/main/java/peoplesoft/logic/commands/job/JobFinalizeCommand.java b/src/main/java/peoplesoft/logic/commands/job/JobFinalizeCommand.java new file mode 100644 index 00000000000..022be794da1 --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/job/JobFinalizeCommand.java @@ -0,0 +1,93 @@ +package peoplesoft.logic.commands.job; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_CONFIRMATION; + +import java.util.List; + +import peoplesoft.commons.core.Messages; +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.model.Model; +import peoplesoft.model.employment.Employment; +import peoplesoft.model.job.Job; +import peoplesoft.model.job.exceptions.JobNotPaidException; +import peoplesoft.model.money.PaymentHandler; +import peoplesoft.model.money.exceptions.PaymentRequiresPersonException; + +/** + * Finalizes a {@code Job} and its associated {@code Payment}s. + */ +public class JobFinalizeCommand extends Command { + + public static final String COMMAND_WORD = "pay"; + + public static final String COMMAND_EXAMPLES = COMMAND_WORD + " 1 " + PREFIX_CONFIRMATION; + + public static final String COMMAND_FORMAT = COMMAND_WORD + " JOB_INDEX " + PREFIX_CONFIRMATION; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Pay out a completed job. Finalizes payments for the job in the job list displayed.\n" + + "Note: This command is irreversible!\n" + + "Format: " + + COMMAND_WORD + " " + + "INDEX " + PREFIX_CONFIRMATION + "\n" + + "Example: " + COMMAND_WORD + " 1" + PREFIX_CONFIRMATION; + + public static final String MESSAGE_SUCCESS = "Finalized payments for Job %s."; + + private final Index toFinalize; + private Employment instance; + + /** + * Creates a {@code JobFinalizeCommand} to finalize a {@code Job}. + * + * @param toFinalize Index of job to finalize. + * @param instance Employment to use (for easier testing). + */ + public JobFinalizeCommand(Index toFinalize, Employment instance) { + requireAllNonNull(toFinalize, instance); + this.toFinalize = toFinalize; + this.instance = instance; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredJobList(); + + if (toFinalize.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MSG_INVALID_JOB_DISPLAYED_IDX); + } + + Job job = lastShownList.get(toFinalize.getZeroBased()); + + if (job.isFinal()) { + throw new CommandException(Messages.MSG_MODIFY_FINAL_JOB); + } + + try { + Job finalJob = job.setAsFinal(); + model.setJob(job, finalJob); + PaymentHandler.finalizePayments(finalJob, model, instance); + } catch (JobNotPaidException e) { + throw new CommandException(Messages.MSG_JOB_NOT_PAID_FAILURE, e); + } catch (PaymentRequiresPersonException e) { + throw new CommandException(Messages.MSG_ASSIGN_PERSON_TO_JOB, e); + } finally { + model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, job.getDesc())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof JobFinalizeCommand // instanceof handles nulls + && toFinalize.equals(((JobFinalizeCommand) other).toFinalize)); + } +} diff --git a/src/main/java/peoplesoft/logic/commands/job/JobFindCommand.java b/src/main/java/peoplesoft/logic/commands/job/JobFindCommand.java new file mode 100644 index 00000000000..2e1972b0cf3 --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/job/JobFindCommand.java @@ -0,0 +1,47 @@ +package peoplesoft.logic.commands.job; + +import static java.util.Objects.requireNonNull; + +import peoplesoft.commons.core.Messages; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.model.Model; +import peoplesoft.model.job.JobContainsKeywordsPredicate; + +public class JobFindCommand extends Command { + + public static final String COMMAND_WORD = "find"; + + public static final String COMMAND_EXAMPLES = COMMAND_WORD + " Painting"; + + public static final String COMMAND_FORMAT = COMMAND_WORD + " KEYWORD"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all jobs whose description contains ALL of " + + "the specified keywords (case-insensitive) and displays them in the list.\n" + + "Format: " + + COMMAND_WORD + " " + + "KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " \"electric aircon appliances\" " + + "finds all jobs which have \"electric\", \"aircon\" and \"appliances\" in them."; + + private final JobContainsKeywordsPredicate predicate; + + public JobFindCommand(JobContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredJobList(predicate); + return new CommandResult( + String.format(Messages.MSG_JOBS_LISTED_OVERVIEW, model.getFilteredJobList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof JobFindCommand // instanceof handles nulls + && predicate.equals(((JobFindCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/peoplesoft/logic/commands/job/JobListCommand.java b/src/main/java/peoplesoft/logic/commands/job/JobListCommand.java new file mode 100644 index 00000000000..3f0877d66b9 --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/job/JobListCommand.java @@ -0,0 +1,30 @@ +package peoplesoft.logic.commands.job; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.model.Model.PREDICATE_SHOW_ALL_JOBS; + +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.model.Model; + +/** + * Lists the {@code Jobs} stored in {@code AddressBook}. + */ +public class JobListCommand extends Command { + + public static final String COMMAND_WORD = "list"; + + public static final String COMMAND_EXAMPLES = "N.A."; + + public static final String COMMAND_FORMAT = COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Listed all jobs"; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + model.updateFilteredJobList(PREDICATE_SHOW_ALL_JOBS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/peoplesoft/logic/commands/job/JobMarkCommand.java b/src/main/java/peoplesoft/logic/commands/job/JobMarkCommand.java new file mode 100644 index 00000000000..e5099aafca8 --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/job/JobMarkCommand.java @@ -0,0 +1,101 @@ +package peoplesoft.logic.commands.job; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; + +import peoplesoft.commons.core.Messages; +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.model.Model; +import peoplesoft.model.employment.Employment; +import peoplesoft.model.job.Job; +import peoplesoft.model.money.PaymentHandler; +import peoplesoft.model.money.exceptions.PaymentRequiresPersonException; + +/** + * Marks a {@code Job} as paid or unpaid. + */ +public class JobMarkCommand extends Command { + + public static final String COMMAND_WORD = "mark"; + + public static final String COMMAND_EXAMPLES = COMMAND_WORD + " 2"; + + public static final String COMMAND_FORMAT = COMMAND_WORD + " JOB_INDEX"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Marks the chosen job as completed.\n" + + "Format: " + COMMAND_WORD + " INDEX" + + "Example: " + COMMAND_WORD + " 2"; + + public static final String MESSAGE_SUCCESS = "Marked job \"%s\" as %s."; + + public static final String MARK_PART = "completed"; + public static final String UNMARK_PART = "not completed"; + + private final Index toMark; + private boolean state; + private Employment instance; + + /** + * Creates a {@code JobMarkCommand} to mark a {@code Job} by {@code Index}. + * + * @param toMark Index of job to mark. + * @param instance Employment to use (for easier testing). + */ + public JobMarkCommand(Index toMark, Employment instance) { + requireAllNonNull(toMark, instance); + this.toMark = toMark; + this.instance = instance; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredJobList(); + + if (toMark.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MSG_INVALID_JOB_DISPLAYED_IDX); + } + + Job jobToMark = lastShownList.get(toMark.getZeroBased()); + + // Todo: Not sure if this should be caught here or later. + if (jobToMark.isFinal()) { + throw new CommandException(Messages.MSG_MODIFY_FINAL_JOB); + } + + try { + if (jobToMark.hasPaid()) { + // Because of implementation of removePendingPayments, the exception will only be thrown + // when there are no persons at all. + PaymentHandler.removePendingPayments(jobToMark, model, instance); + model.setJob(jobToMark, jobToMark.setAsNotPaid()); + state = true; + } else { + PaymentHandler.createPendingPayments(jobToMark, model, instance); + model.setJob(jobToMark, jobToMark.setAsPaid()); + state = false; + } + } catch (PaymentRequiresPersonException e) { + throw new CommandException(Messages.MSG_ASSIGN_PERSON_TO_JOB, e); + } finally { + // Turns out model equals() tests filtered lists + model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, jobToMark.getDesc(), + state ? UNMARK_PART : MARK_PART)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof JobMarkCommand // instanceof handles nulls + && toMark.equals(((JobMarkCommand) other).toMark)); + } +} diff --git a/src/main/java/peoplesoft/logic/commands/person/PersonAddCommand.java b/src/main/java/peoplesoft/logic/commands/person/PersonAddCommand.java new file mode 100644 index 00000000000..efc43da05c4 --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/person/PersonAddCommand.java @@ -0,0 +1,90 @@ +package peoplesoft.logic.commands.person; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_EMAIL; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_NAME; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_PHONE; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_RATE; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_TAG; + +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.model.Model; +import peoplesoft.model.person.Person; + +/** + * Adds a person to the database. + */ +public class PersonAddCommand extends Command { + + public static final String COMMAND_WORD = "personadd"; + + public static final String COMMAND_EXAMPLES = COMMAND_WORD + " " + + PREFIX_NAME + "Nicole Tan " + + PREFIX_PHONE + "99338558 " + + PREFIX_EMAIL + "nicole@stffhub.org " + + PREFIX_ADDRESS + "1 Tech Drive, S138572 " + + PREFIX_RATE + "37.50 " + + PREFIX_TAG + "Hardware " + + PREFIX_TAG + "Senior"; + + public static final String COMMAND_FORMAT = COMMAND_WORD + " " + + PREFIX_NAME + "NAME " + + PREFIX_PHONE + "PHONE " + + PREFIX_EMAIL + "EMAIL " + + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_RATE + "RATE " + + "[" + PREFIX_TAG + "TAG]..."; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the database.\n" + + "Format: " + + COMMAND_WORD + " " + + PREFIX_NAME + "NAME " + + PREFIX_PHONE + "PHONE " + + PREFIX_EMAIL + "EMAIL " + + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_RATE + "RATE " + + "[" + 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_RATE + "3.20 " + + PREFIX_TAG + "Intern " + + PREFIX_TAG + "Painting"; + + public static final String MESSAGE_SUCCESS = "%s was added."; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the database"; + + private final Person toAdd; + + /** + * Creates an PersonAddCommand to add the specified {@code Person} + */ + public PersonAddCommand(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.getName())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PersonAddCommand // instanceof handles nulls + && toAdd.equals(((PersonAddCommand) other).toAdd)); + } +} diff --git a/src/main/java/peoplesoft/logic/commands/person/PersonDeleteCommand.java b/src/main/java/peoplesoft/logic/commands/person/PersonDeleteCommand.java new file mode 100644 index 00000000000..eb035dd090a --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/person/PersonDeleteCommand.java @@ -0,0 +1,62 @@ +package peoplesoft.logic.commands.person; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import peoplesoft.commons.core.Messages; +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.model.Model; +import peoplesoft.model.employment.Employment; +import peoplesoft.model.person.Person; + +/** + * Deletes a person identified using it's displayed index from the database. + */ +public class PersonDeleteCommand extends Command { + + public static final String COMMAND_WORD = "persondelete"; + + public static final String COMMAND_EXAMPLES = COMMAND_WORD + " 3"; + + public static final String COMMAND_FORMAT = COMMAND_WORD + " PERSON_INDEX"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the person identified by the index number used in the displayed person list.\n" + + "Format: " + COMMAND_WORD + " INDEX\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "%s was removed."; + + private final Index targetIndex; + + public PersonDeleteCommand(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.MSG_INVALID_PERSON_DISPLAYED_IDX); + } + + Person p = lastShownList.get(targetIndex.getZeroBased()); // p is the person to delete + model.deletePerson(p); + // Deletes employment associations + Employment.getInstance().deletePerson(p); + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, p.getName())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PersonDeleteCommand // instanceof handles nulls + && targetIndex.equals(((PersonDeleteCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/peoplesoft/logic/commands/person/PersonEditCommand.java similarity index 67% rename from src/main/java/seedu/address/logic/commands/EditCommand.java rename to src/main/java/peoplesoft/logic/commands/person/PersonEditCommand.java index 7e36114902f..dffabf2758b 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/peoplesoft/logic/commands/person/PersonEditCommand.java @@ -1,54 +1,75 @@ -package seedu.address.logic.commands; +package peoplesoft.logic.commands.person; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_EMAIL; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_NAME; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_PHONE; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_RATE; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_TAG; +import static peoplesoft.model.Model.PREDICATE_SHOW_ALL_PERSONS; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; 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; +import peoplesoft.commons.core.Messages; +import peoplesoft.commons.core.index.Index; +import peoplesoft.commons.util.CollectionUtil; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.model.Model; +import peoplesoft.model.money.Payment; +import peoplesoft.model.money.Rate; +import peoplesoft.model.person.Address; +import peoplesoft.model.person.Email; +import peoplesoft.model.person.Name; +import peoplesoft.model.person.Person; +import peoplesoft.model.person.Phone; +import peoplesoft.model.tag.Tag; +import peoplesoft.model.util.ID; /** - * Edits the details of an existing person in the address book. + * Edits the details of an existing person in the database. */ -public class EditCommand extends Command { +public class PersonEditCommand extends Command { - public static final String COMMAND_WORD = "edit"; + public static final String COMMAND_WORD = "personedit"; + + public static final String COMMAND_EXAMPLES = COMMAND_WORD + " 2 " + + PREFIX_NAME + "Nicole Lee " + + PREFIX_TAG + "OS"; + + public static final String COMMAND_FORMAT = COMMAND_WORD + " PERSON_INDEX (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_PHONE + "PHONE] " + + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_RATE + "RATE] " + + "[" + PREFIX_TAG + "TAG]..."; 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) " + + "Existing values and tags will be overwritten by the input values.\n" + + "Format: " + + COMMAND_WORD + " INDEX " + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_RATE + "RATE] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " + PREFIX_EMAIL + "johndoe@example.com"; - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; + public static final String MESSAGE_EDIT_PERSON_SUCCESS = "The details of %s were edited."; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the database."; private final Index index; private final EditPersonDescriptor editPersonDescriptor; @@ -57,7 +78,7 @@ public class EditCommand extends Command { * @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) { + public PersonEditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { requireNonNull(index); requireNonNull(editPersonDescriptor); @@ -71,7 +92,7 @@ public CommandResult execute(Model model) throws CommandException { List lastShownList = model.getFilteredPersonList(); if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + throw new CommandException(Messages.MSG_INVALID_PERSON_DISPLAYED_IDX); } Person personToEdit = lastShownList.get(index.getZeroBased()); @@ -83,7 +104,7 @@ public CommandResult execute(Model model) throws CommandException { model.setPerson(personToEdit, editedPerson); 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, editedPerson.getName())); } /** @@ -97,9 +118,12 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + Rate updatedRate = editPersonDescriptor.getRate().orElse(personToEdit.getRate()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + Map payments = personToEdit.getPayments(); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(personToEdit.getPersonId(), + updatedName, updatedPhone, updatedEmail, updatedAddress, updatedRate, updatedTags, payments); } @Override @@ -110,12 +134,12 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof EditCommand)) { + if (!(other instanceof PersonEditCommand)) { return false; } // state check - EditCommand e = (EditCommand) other; + PersonEditCommand e = (PersonEditCommand) other; return index.equals(e.index) && editPersonDescriptor.equals(e.editPersonDescriptor); } @@ -129,6 +153,7 @@ public static class EditPersonDescriptor { private Phone phone; private Email email; private Address address; + private Rate rate; private Set tags; public EditPersonDescriptor() {} @@ -142,6 +167,7 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setPhone(toCopy.phone); setEmail(toCopy.email); setAddress(toCopy.address); + setRate(toCopy.rate); setTags(toCopy.tags); } @@ -149,7 +175,7 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, address, rate, tags); } public void setName(Name name) { @@ -184,6 +210,14 @@ public Optional

getAddress() { return Optional.ofNullable(address); } + public void setRate(Rate rate) { + this.rate = rate; + } + + public Optional getRate() { + return Optional.ofNullable(rate); + } + /** * Sets {@code tags} to this object's {@code tags}. * A defensive copy of {@code tags} is used internally. @@ -220,6 +254,7 @@ public boolean equals(Object other) { && getPhone().equals(e.getPhone()) && getEmail().equals(e.getEmail()) && getAddress().equals(e.getAddress()) + && getRate().equals(e.getRate()) && getTags().equals(e.getTags()); } } diff --git a/src/main/java/peoplesoft/logic/commands/person/PersonFindCommand.java b/src/main/java/peoplesoft/logic/commands/person/PersonFindCommand.java new file mode 100644 index 00000000000..b3870d8bffe --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/person/PersonFindCommand.java @@ -0,0 +1,51 @@ +package peoplesoft.logic.commands.person; + +import static java.util.Objects.requireNonNull; + +import peoplesoft.commons.core.Messages; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.model.Model; +import peoplesoft.model.person.PersonContainsKeywordsPredicate; + +/** + * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class PersonFindCommand extends Command { + + public static final String COMMAND_WORD = "personfind"; + + public static final String COMMAND_EXAMPLES = COMMAND_WORD + " Aircon,\n" + + COMMAND_WORD + " Nicole Hardware"; + + public static final String COMMAND_FORMAT = COMMAND_WORD + " KEYWORD [MORE_KEYWORDS]..."; + + 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" + + "Format: " + + COMMAND_WORD + " " + + "KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " alice bob charlie"; + + private final PersonContainsKeywordsPredicate predicate; + + public PersonFindCommand(PersonContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(predicate); + return new CommandResult( + String.format(Messages.MSG_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PersonFindCommand // instanceof handles nulls + && predicate.equals(((PersonFindCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/peoplesoft/logic/commands/person/PersonListCommand.java b/src/main/java/peoplesoft/logic/commands/person/PersonListCommand.java new file mode 100644 index 00000000000..ec0f43e6438 --- /dev/null +++ b/src/main/java/peoplesoft/logic/commands/person/PersonListCommand.java @@ -0,0 +1,29 @@ +package peoplesoft.logic.commands.person; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.model.Model; + +/** + * Lists all persons in the address book to the user. + */ +public class PersonListCommand extends Command { + + public static final String COMMAND_WORD = "personlist"; + + public static final String COMMAND_EXAMPLES = "N.A."; + + public static final String COMMAND_FORMAT = COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "All people are now listed under Employees."; + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/peoplesoft/logic/export/Exporter.java b/src/main/java/peoplesoft/logic/export/Exporter.java new file mode 100644 index 00000000000..310133f18ef --- /dev/null +++ b/src/main/java/peoplesoft/logic/export/Exporter.java @@ -0,0 +1,98 @@ +package peoplesoft.logic.export; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +import peoplesoft.model.Model; +import peoplesoft.model.employment.Employment; +import peoplesoft.model.job.Job; +import peoplesoft.model.money.Money; +import peoplesoft.model.money.Payment; +import peoplesoft.model.person.Person; + + +public class Exporter { + + private static final DateTimeFormatter dtFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_hh-mm-ss"); + + private final Person personToExport; + + private final Model model; + + private final File storageFile; + + private final String targetFileName; + + private Exporter(Person personToExport, Model model) { + this.personToExport = personToExport; + this.model = model; + this.targetFileName = (personToExport.getName().toString() // assume only alphanumeric + spaces + .replace(" ", "-")) // just in case + + "_" + dtFormatter.format(LocalDateTime.now()) + ".csv"; + this.storageFile = Path.of("data", this.targetFileName).toFile(); + } + + public static Exporter getNewInstance(Person personToExport, Model model) { + return new Exporter(personToExport, model); + } + + // https://www.baeldung.com/java-csv + private static String escape(String data) { + String escapedData = data.replaceAll("\\R", " "); + if (data.contains(",") || data.contains("\"") || data.contains("'")) { + data = data.replace("\"", "\"\""); + escapedData = "\"" + data + "\""; + } + return escapedData; + } + + private static String toCsvRow(Job job, Person person) { + Payment pymt = person.getPayments().getOrDefault( + job.getJobId(), + Payment.createPayment(person, job, new Money(0))); + BigDecimal rate = pymt.getAmount().divide(BigDecimal.valueOf(job.getDuration().toHours())).getValue(); + + String status = job.hasPaid() + ? pymt.isCompleted() + ? "Paid" + : "Pending payment" + : "Incomplete"; + + // jobid, jobdesc, status, rate, duration, payment + return String.format("%s,%s,%s,$%s/h,%dh,%s", + job.getJobId(), + escape(job.getDesc()), + status, + rate.toPlainString(), + job.getDuration().toHours(), + pymt.getAmount().toString()); + } + + /** + * Exports to a file with the name of the person + * @throws IOException + */ + public void export() throws IOException { + List jobsAssignedToPerson = Employment.getInstance().getJobs(personToExport, model); + + String listHeader = "Job ID,Job Description,Status,Rate,Duration,Payment"; + + String incomeItemized = jobsAssignedToPerson.stream() + .map(job -> toCsvRow(job, personToExport)) + .collect(Collectors.joining("\n")); + + String exportableMessage = listHeader + + "\n" + incomeItemized; + + FileWriter fileWriter = new FileWriter(storageFile); + fileWriter.write(exportableMessage); + fileWriter.close(); + } +} diff --git a/src/main/java/peoplesoft/logic/parser/AddressBookParser.java b/src/main/java/peoplesoft/logic/parser/AddressBookParser.java new file mode 100644 index 00000000000..3129cf3e18a --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/AddressBookParser.java @@ -0,0 +1,120 @@ +package peoplesoft.logic.parser; + +import static peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; +import static peoplesoft.commons.core.Messages.MSG_UNKNOWN_CMD; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import peoplesoft.logic.commands.ClearCommand; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.commands.ExitCommand; +import peoplesoft.logic.commands.ExportCommand; +import peoplesoft.logic.commands.HelpCommand; +import peoplesoft.logic.commands.job.JobAddCommand; +import peoplesoft.logic.commands.job.JobAssignCommand; +import peoplesoft.logic.commands.job.JobDeleteCommand; +import peoplesoft.logic.commands.job.JobFinalizeCommand; +import peoplesoft.logic.commands.job.JobFindCommand; +import peoplesoft.logic.commands.job.JobListCommand; +import peoplesoft.logic.commands.job.JobMarkCommand; +import peoplesoft.logic.commands.person.PersonAddCommand; +import peoplesoft.logic.commands.person.PersonDeleteCommand; +import peoplesoft.logic.commands.person.PersonEditCommand; +import peoplesoft.logic.commands.person.PersonFindCommand; +import peoplesoft.logic.commands.person.PersonListCommand; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.logic.parser.job.JobAddCommandParser; +import peoplesoft.logic.parser.job.JobAssignCommandParser; +import peoplesoft.logic.parser.job.JobDeleteCommandParser; +import peoplesoft.logic.parser.job.JobFinalizeCommandParser; +import peoplesoft.logic.parser.job.JobFindCommandParser; +import peoplesoft.logic.parser.job.JobMarkCommandParser; +import peoplesoft.logic.parser.person.PersonAddCommandParser; +import peoplesoft.logic.parser.person.PersonDeleteCommandParser; +import peoplesoft.logic.parser.person.PersonEditCommandParser; +import peoplesoft.logic.parser.person.PersonFindCommandParser; + +/** + * Parses user input. + */ +public class AddressBookParser { + + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MSG_INVALID_CMD_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + switch (commandWord) { + + case PersonAddCommand.COMMAND_WORD: + return new PersonAddCommandParser().parse(arguments); + + case PersonEditCommand.COMMAND_WORD: + return new PersonEditCommandParser().parse(arguments); + + case PersonDeleteCommand.COMMAND_WORD: + return new PersonDeleteCommandParser().parse(arguments); + + case ExportCommand.COMMAND_WORD: + return new ExportCommandParser().parse(arguments); + + case ClearCommand.COMMAND_WORD: + return new ClearCommand(); + + case PersonFindCommand.COMMAND_WORD: + return new PersonFindCommandParser().parse(arguments); + + case PersonListCommand.COMMAND_WORD: + return new PersonListCommand(); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + // Job related commands + + case JobAddCommand.COMMAND_WORD: + return new JobAddCommandParser().parse(arguments); + + case JobListCommand.COMMAND_WORD: + return new JobListCommand(); + + case JobDeleteCommand.COMMAND_WORD: + return new JobDeleteCommandParser().parse(arguments); + + case JobMarkCommand.COMMAND_WORD: + return new JobMarkCommandParser().parse(arguments); + + case JobFindCommand.COMMAND_WORD: + return new JobFindCommandParser().parse(arguments); + + case JobAssignCommand.COMMAND_WORD: + return new JobAssignCommandParser().parse(arguments); + + case JobFinalizeCommand.COMMAND_WORD: + return new JobFinalizeCommandParser().parse(arguments); + + default: + throw new ParseException(MSG_UNKNOWN_CMD); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/peoplesoft/logic/parser/ArgumentMultimap.java similarity index 98% rename from src/main/java/seedu/address/logic/parser/ArgumentMultimap.java rename to src/main/java/peoplesoft/logic/parser/ArgumentMultimap.java index 954c8e18f8e..55bb9c3a02e 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/peoplesoft/logic/parser/ArgumentMultimap.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package peoplesoft.logic.parser; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/peoplesoft/logic/parser/ArgumentTokenizer.java similarity index 99% rename from src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java rename to src/main/java/peoplesoft/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..d45cf698cd5 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/peoplesoft/logic/parser/ArgumentTokenizer.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package peoplesoft.logic.parser; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/peoplesoft/logic/parser/CliSyntax.java similarity index 61% rename from src/main/java/seedu/address/logic/parser/CliSyntax.java rename to src/main/java/peoplesoft/logic/parser/CliSyntax.java index 75b1a9bf119..52ce0a2b4e1 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/peoplesoft/logic/parser/CliSyntax.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package peoplesoft.logic.parser; /** * Contains Command Line Interface (CLI) syntax definitions common to multiple commands @@ -11,5 +11,8 @@ public class CliSyntax { public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); - + public static final Prefix PREFIX_RATE = new Prefix("r/"); + public static final Prefix PREFIX_DURATION = new Prefix("d/"); + public static final Prefix PREFIX_INDEX = new Prefix("i/"); + public static final Prefix PREFIX_CONFIRMATION = new Prefix("y/"); } diff --git a/src/main/java/peoplesoft/logic/parser/ExportCommandParser.java b/src/main/java/peoplesoft/logic/parser/ExportCommandParser.java new file mode 100644 index 00000000000..10348356e71 --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/ExportCommandParser.java @@ -0,0 +1,29 @@ +package peoplesoft.logic.parser; + +import static peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; + +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.ExportCommand; +import peoplesoft.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ExportCommand object + */ +public class ExportCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ExportCommand + * and returns a ExportCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ExportCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new ExportCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MSG_INVALID_CMD_FORMAT, ExportCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/peoplesoft/logic/parser/Parser.java similarity index 72% rename from src/main/java/seedu/address/logic/parser/Parser.java rename to src/main/java/peoplesoft/logic/parser/Parser.java index d6551ad8e3f..81ad476335d 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/peoplesoft/logic/parser/Parser.java @@ -1,7 +1,7 @@ -package seedu.address.logic.parser; +package peoplesoft.logic.parser; -import seedu.address.logic.commands.Command; -import seedu.address.logic.parser.exceptions.ParseException; +import peoplesoft.logic.commands.Command; +import peoplesoft.logic.parser.exceptions.ParseException; /** * Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}. diff --git a/src/main/java/peoplesoft/logic/parser/ParserUtil.java b/src/main/java/peoplesoft/logic/parser/ParserUtil.java new file mode 100644 index 00000000000..fb3e9bd8c9b --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/ParserUtil.java @@ -0,0 +1,242 @@ +package peoplesoft.logic.parser; + +import static java.util.Objects.requireNonNull; + +import java.math.BigDecimal; +import java.time.Duration; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import peoplesoft.commons.core.Messages; +import peoplesoft.commons.core.index.Index; +import peoplesoft.commons.util.StringUtil; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.money.Money; +import peoplesoft.model.money.Rate; +import peoplesoft.model.money.exceptions.NegativeMoneyValueException; +import peoplesoft.model.person.Address; +import peoplesoft.model.person.Email; +import peoplesoft.model.person.Name; +import peoplesoft.model.person.Phone; +import peoplesoft.model.tag.Tag; +import peoplesoft.model.util.ID; + +/** + * Contains utility methods used for parsing strings in the various *Parser classes. + */ +public class ParserUtil { + + /** + * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be + * trimmed. + * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + */ + public static Index parseIndex(String oneBasedIndex) throws ParseException { + requireNonNull(oneBasedIndex); + String trimmedIndex = oneBasedIndex.trim(); + if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + throw new ParseException(Index.MESSAGE_CONSTRAINTS); + } + return Index.fromOneBased(Integer.parseInt(trimmedIndex)); + } + + /** + * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be + * trimmed. + * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + */ + public static Set parseIndexes(Collection oneBasedIndexes) throws ParseException { + requireNonNull(oneBasedIndexes); + final Set indexSet = new HashSet<>(); + for (String index : oneBasedIndexes) { + indexSet.add(parseIndex(index)); + } + return indexSet; + } + + /** + * Parses a {@code String name} into a {@code Name}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code name} is invalid. + */ + public static Name parseName(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!Name.isValidName(trimmedName)) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS); + } + return new Name(trimmedName); + } + + /** + * Parses a {@code String phone} into a {@code Phone}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code phone} is invalid. + */ + public static Phone parsePhone(String phone) throws ParseException { + requireNonNull(phone); + String trimmedPhone = phone.trim(); + if (!Phone.isValidPhone(trimmedPhone)) { + throw new ParseException(Phone.MESSAGE_CONSTRAINTS); + } + return new Phone(trimmedPhone); + } + + /** + * Parses a {@code String address} into an {@code Address}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code address} is invalid. + */ + public static Address parseAddress(String address) throws ParseException { + requireNonNull(address); + String trimmedAddress = address.trim(); + if (!Address.isValidAddress(trimmedAddress)) { + throw new ParseException(Address.MESSAGE_CONSTRAINTS); + } + return new Address(trimmedAddress); + } + + /** + * Parses a {@code String email} into an {@code Email}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code email} is invalid. + */ + public static Email parseEmail(String email) throws ParseException { + requireNonNull(email); + String trimmedEmail = email.trim(); + if (!Email.isValidEmail(trimmedEmail)) { + throw new ParseException(Email.MESSAGE_CONSTRAINTS); + } + return new Email(trimmedEmail); + } + + /** + * Parses a {@code String tag} into a {@code Tag}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code tag} is invalid. + */ + public static Tag parseTag(String tag) throws ParseException { + requireNonNull(tag); + String trimmedTag = tag.trim(); + if (!Tag.isValidTagName(trimmedTag)) { + throw new ParseException(Tag.MESSAGE_CONSTRAINTS); + } + return new Tag(trimmedTag); + } + + /** + * Parses {@code Collection tags} into a {@code Set}. + */ + public static Set parseTags(Collection tags) throws ParseException { + requireNonNull(tags); + final Set tagSet = new HashSet<>(); + for (String tagName : tags) { + tagSet.add(parseTag(tagName)); + } + return tagSet; + } + + /** + * Parses a {@code String}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code string} is invalid. + */ + public static String parseString(String str) throws ParseException { + requireNonNull(str); + String res = str.trim(); + if (res.isBlank()) { + throw new ParseException(Messages.MSG_EMPTY_STRING); + } + return res; + } + + /** + * Parses a {@code Rate}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code rate} is invalid. + */ + public static Rate parseRate(String str) throws ParseException { + requireNonNull(str); + String trim = str.trim(); + Rate res; + try { + res = new Rate(new Money(Double.parseDouble(trim)), Duration.ofHours(1)); + } catch (NumberFormatException | NegativeMoneyValueException e) { + // TODO: add message/complex rate parsing %s/%s + throw new ParseException(Rate.MESSAGE_CONSTRAINTS, e); + } + if (isRateTooLarge(res)) { + throw new ParseException(Rate.MESSAGE_TOO_LARGE); + } + return res; + } + + /** + * Returns if rate is too large. + */ + private static boolean isRateTooLarge(Rate rate) { + return rate.getAmount().getValue().compareTo(BigDecimal.valueOf(1000000)) > 0; + } + + /** + * Parses a {@code Duration}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code duration} is invalid. + */ + public static Duration parseDuration(String str) throws ParseException { + requireNonNull(str); + String trim = str.trim(); + double dur; + try { + dur = Double.parseDouble(trim); + } catch (NumberFormatException e) { + throw new ParseException(Messages.MSG_DURATION_CONSTRAINTS, e); + } + Duration res = Duration.ofMinutes(Math.round(dur * 60)); + if (!isDurationNonNegative(res)) { + throw new ParseException(Messages.MSG_DURATION_CONSTRAINTS); + } + if (isDurationTooLarge(res)) { + throw new ParseException(Messages.MSG_DURATION_TOO_LARGE); + } + return res; + } + + /** + * Returns if duration is greater than zero. + */ + private static boolean isDurationNonNegative(Duration dur) { + return dur.compareTo(Duration.ZERO) > 0; + } + + /** + * Returns if duration is too large. + */ + private static boolean isDurationTooLarge(Duration dur) { + return dur.compareTo(Duration.ofHours(100000)) > 0; + } + + /** + * Parses a {@code String id} into an {@code ID}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code id} is invalid. + */ + public static ID parseID(String id) throws ParseException { + requireNonNull(id); + String trimmedId = id.trim(); + if (!ID.isValidId(trimmedId)) { + throw new ParseException(ID.MESSAGE_CONSTRAINTS); + } + return new ID(trimmedId); + } +} diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/peoplesoft/logic/parser/Prefix.java similarity index 95% rename from src/main/java/seedu/address/logic/parser/Prefix.java rename to src/main/java/peoplesoft/logic/parser/Prefix.java index c859d5fa5db..185cf9d4971 100644 --- a/src/main/java/seedu/address/logic/parser/Prefix.java +++ b/src/main/java/peoplesoft/logic/parser/Prefix.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package peoplesoft.logic.parser; /** * A prefix that marks the beginning of an argument in an arguments string. diff --git a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java b/src/main/java/peoplesoft/logic/parser/exceptions/ParseException.java similarity index 73% rename from src/main/java/seedu/address/logic/parser/exceptions/ParseException.java rename to src/main/java/peoplesoft/logic/parser/exceptions/ParseException.java index 158a1a54c1c..1c30bf52fd4 100644 --- a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java +++ b/src/main/java/peoplesoft/logic/parser/exceptions/ParseException.java @@ -1,6 +1,6 @@ -package seedu.address.logic.parser.exceptions; +package peoplesoft.logic.parser.exceptions; -import seedu.address.commons.exceptions.IllegalValueException; +import peoplesoft.commons.exceptions.IllegalValueException; /** * Represents a parse error encountered by a parser. diff --git a/src/main/java/peoplesoft/logic/parser/job/JobAddCommandParser.java b/src/main/java/peoplesoft/logic/parser/job/JobAddCommandParser.java new file mode 100644 index 00000000000..03b3c8aa6e8 --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/job/JobAddCommandParser.java @@ -0,0 +1,55 @@ +package peoplesoft.logic.parser.job; + +import static peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_DURATION; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_NAME; + +import java.time.Duration; +import java.util.stream.Stream; + +import peoplesoft.commons.core.JobIdFactory; +import peoplesoft.logic.commands.job.JobAddCommand; +import peoplesoft.logic.parser.ArgumentMultimap; +import peoplesoft.logic.parser.ArgumentTokenizer; +import peoplesoft.logic.parser.Parser; +import peoplesoft.logic.parser.ParserUtil; +import peoplesoft.logic.parser.Prefix; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.job.Job; +import peoplesoft.model.util.ID; + +/** + * Parses input parameters and returns a {@code Job}. + */ +public class JobAddCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the {@code JobAddCommand} + * and returns a {@code JobAddCommand} object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public JobAddCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_DURATION); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_DURATION) + || !argMultimap.getPreamble().isBlank()) { + throw new ParseException(String.format(MSG_INVALID_CMD_FORMAT, + JobAddCommand.MESSAGE_USAGE)); + } + String name = ParserUtil.parseString(argMultimap.getValue(PREFIX_NAME).get()); + Duration duration = ParserUtil.parseDuration(argMultimap.getValue(PREFIX_DURATION).get()); + ID id = JobIdFactory.nextId(); + + Job toAdd = new Job(id, name, duration); + + return new JobAddCommand(toAdd); + } + + /** + * 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/peoplesoft/logic/parser/job/JobAssignCommandParser.java b/src/main/java/peoplesoft/logic/parser/job/JobAssignCommandParser.java new file mode 100644 index 00000000000..7a418b87c9c --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/job/JobAssignCommandParser.java @@ -0,0 +1,57 @@ +package peoplesoft.logic.parser.job; + +import static peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_INDEX; + +import java.util.Set; +import java.util.stream.Stream; + +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.job.JobAssignCommand; +import peoplesoft.logic.parser.ArgumentMultimap; +import peoplesoft.logic.parser.ArgumentTokenizer; +import peoplesoft.logic.parser.Parser; +import peoplesoft.logic.parser.ParserUtil; +import peoplesoft.logic.parser.Prefix; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.employment.Employment; + +/** + * Parses an {@code Index} for {@code Job} and {@code Indexes} for {@code Person}. + */ +public class JobAssignCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the {@code JobAssignCommand} + * and returns an {@code ArgumentMultimap} for {@code JobAssignCommand}. + * @throws ParseException if the user input does not conform the expected format + */ + public JobAssignCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_INDEX); + + if (!arePrefixesPresent(argMultimap, PREFIX_INDEX) + || argMultimap.getPreamble().isBlank()) { + throw new ParseException(String.format(MSG_INVALID_CMD_FORMAT, + JobAssignCommand.MESSAGE_USAGE)); + } + Index jobIndex; + Set personIndexes; + + try { + jobIndex = ParserUtil.parseIndex(argMultimap.getPreamble()); + personIndexes = ParserUtil.parseIndexes(argMultimap.getAllValues(PREFIX_INDEX)); + } catch (ParseException e) { + throw new ParseException(String.format(MSG_INVALID_CMD_FORMAT, + JobAssignCommand.MESSAGE_USAGE)); + } + + return new JobAssignCommand(jobIndex, personIndexes, Employment.getInstance()); + } + + /** + * 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/peoplesoft/logic/parser/job/JobDeleteCommandParser.java b/src/main/java/peoplesoft/logic/parser/job/JobDeleteCommandParser.java new file mode 100644 index 00000000000..279dbc9b6be --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/job/JobDeleteCommandParser.java @@ -0,0 +1,29 @@ +package peoplesoft.logic.parser.job; + +import static peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; + +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.job.JobDeleteCommand; +import peoplesoft.logic.parser.Parser; +import peoplesoft.logic.parser.ParserUtil; +import peoplesoft.logic.parser.exceptions.ParseException; + +/** + * Parses an {@code Index} to delete. + */ +public class JobDeleteCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the {@code JobDeleteCommand} + * and returns a {@code JobDeleteCommand} object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public JobDeleteCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new JobDeleteCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MSG_INVALID_CMD_FORMAT, JobDeleteCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/peoplesoft/logic/parser/job/JobFinalizeCommandParser.java b/src/main/java/peoplesoft/logic/parser/job/JobFinalizeCommandParser.java new file mode 100644 index 00000000000..03680305adc --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/job/JobFinalizeCommandParser.java @@ -0,0 +1,51 @@ +package peoplesoft.logic.parser.job; + +import static peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_CONFIRMATION; + +import java.util.stream.Stream; + +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.job.JobFinalizeCommand; +import peoplesoft.logic.parser.ArgumentMultimap; +import peoplesoft.logic.parser.ArgumentTokenizer; +import peoplesoft.logic.parser.Parser; +import peoplesoft.logic.parser.ParserUtil; +import peoplesoft.logic.parser.Prefix; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.employment.Employment; + +/** + * Parses an {@code Index} of a {@code Job} to finalize. + */ +public class JobFinalizeCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the {@code JobFinalizeCommand} + * and returns a {@code JobFinalizeCommand} object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public JobFinalizeCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_CONFIRMATION); + + if (!arePrefixesPresent(argMultimap, PREFIX_CONFIRMATION) || argMultimap.getPreamble().isBlank() + || !argMultimap.getValue(PREFIX_CONFIRMATION).get().isBlank()) { + throw new ParseException(String.format(MSG_INVALID_CMD_FORMAT, + JobFinalizeCommand.MESSAGE_USAGE)); + } + try { + Index index = ParserUtil.parseIndex(argMultimap.getPreamble()); + return new JobFinalizeCommand(index, Employment.getInstance()); + } catch (ParseException pe) { + throw new ParseException( + String.format(MSG_INVALID_CMD_FORMAT, JobFinalizeCommand.MESSAGE_USAGE), pe); + } + } + + /** + * 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/peoplesoft/logic/parser/job/JobFindCommandParser.java b/src/main/java/peoplesoft/logic/parser/job/JobFindCommandParser.java new file mode 100644 index 00000000000..afb64fac9c9 --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/job/JobFindCommandParser.java @@ -0,0 +1,33 @@ +package peoplesoft.logic.parser.job; + +import static peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; + +import java.util.Arrays; + +import peoplesoft.logic.commands.job.JobFindCommand; +import peoplesoft.logic.parser.Parser; +import peoplesoft.logic.parser.ParserUtil; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.job.JobContainsKeywordsPredicate; + +public class JobFindCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the {@code JobFindCommand} + * and returns a {@code JobFindCommand} object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public JobFindCommand parse(String args) throws ParseException { + String trimmedArgs; + try { + trimmedArgs = ParserUtil.parseString(args); + } catch (ParseException pe) { + throw new ParseException( + String.format(MSG_INVALID_CMD_FORMAT, JobFindCommand.MESSAGE_USAGE), pe); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + + return new JobFindCommand(new JobContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + } +} diff --git a/src/main/java/peoplesoft/logic/parser/job/JobMarkCommandParser.java b/src/main/java/peoplesoft/logic/parser/job/JobMarkCommandParser.java new file mode 100644 index 00000000000..bb475e0e1c3 --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/job/JobMarkCommandParser.java @@ -0,0 +1,30 @@ +package peoplesoft.logic.parser.job; + +import static peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; + +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.job.JobMarkCommand; +import peoplesoft.logic.parser.Parser; +import peoplesoft.logic.parser.ParserUtil; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.employment.Employment; + +/** + * Parses an {@code Index} of a {@code Job} to mark. + */ +public class JobMarkCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the {@code JobMarkCommand} + * and returns a {@code JobMarkCommand} object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public JobMarkCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new JobMarkCommand(index, Employment.getInstance()); + } catch (ParseException pe) { + throw new ParseException( + String.format(MSG_INVALID_CMD_FORMAT, JobMarkCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/peoplesoft/logic/parser/person/PersonAddCommandParser.java b/src/main/java/peoplesoft/logic/parser/person/PersonAddCommandParser.java new file mode 100644 index 00000000000..71f8af812fd --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/person/PersonAddCommandParser.java @@ -0,0 +1,74 @@ +package peoplesoft.logic.parser.person; + +import static peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_EMAIL; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_NAME; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_PHONE; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_RATE; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import peoplesoft.commons.core.PersonIdFactory; +import peoplesoft.logic.commands.person.PersonAddCommand; +import peoplesoft.logic.parser.ArgumentMultimap; +import peoplesoft.logic.parser.ArgumentTokenizer; +import peoplesoft.logic.parser.Parser; +import peoplesoft.logic.parser.ParserUtil; +import peoplesoft.logic.parser.Prefix; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.money.Payment; +import peoplesoft.model.money.Rate; +import peoplesoft.model.person.Address; +import peoplesoft.model.person.Email; +import peoplesoft.model.person.Name; +import peoplesoft.model.person.Person; +import peoplesoft.model.person.Phone; +import peoplesoft.model.tag.Tag; +import peoplesoft.model.util.ID; + +/** + * Parses input arguments and creates a new PersonAddCommand object + */ +public class PersonAddCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the PersonAddCommand + * and returns an PersonAddCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public PersonAddCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_RATE, PREFIX_TAG); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_RATE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MSG_INVALID_CMD_FORMAT, PersonAddCommand.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()); + Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + Rate rate = ParserUtil.parseRate(argMultimap.getValue(PREFIX_RATE).get()); + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + Map payments = Map.of(); + + Person person = new Person(PersonIdFactory.nextId(), name, phone, email, address, rate, tagList, payments); + + return new PersonAddCommand(person); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/peoplesoft/logic/parser/person/PersonDeleteCommandParser.java b/src/main/java/peoplesoft/logic/parser/person/PersonDeleteCommandParser.java new file mode 100644 index 00000000000..a4f68957380 --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/person/PersonDeleteCommandParser.java @@ -0,0 +1,31 @@ +package peoplesoft.logic.parser.person; + +import static peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; + +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.person.PersonDeleteCommand; +import peoplesoft.logic.parser.Parser; +import peoplesoft.logic.parser.ParserUtil; +import peoplesoft.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new PersonDeleteCommand object + */ +public class PersonDeleteCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the PersonDeleteCommand + * and returns a PersonDeleteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public PersonDeleteCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new PersonDeleteCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MSG_INVALID_CMD_FORMAT , PersonDeleteCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/peoplesoft/logic/parser/person/PersonEditCommandParser.java similarity index 57% rename from src/main/java/seedu/address/logic/parser/EditCommandParser.java rename to src/main/java/peoplesoft/logic/parser/person/PersonEditCommandParser.java index 845644b7dea..f28a0f99209 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/peoplesoft/logic/parser/person/PersonEditCommandParser.java @@ -1,45 +1,52 @@ -package seedu.address.logic.parser; +package peoplesoft.logic.parser.person; 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 peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_EMAIL; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_NAME; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_PHONE; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_RATE; +import static peoplesoft.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Collection; import java.util.Collections; import java.util.Optional; import java.util.Set; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; +import peoplesoft.commons.core.index.Index; +import peoplesoft.logic.commands.person.PersonEditCommand; +import peoplesoft.logic.commands.person.PersonEditCommand.EditPersonDescriptor; +import peoplesoft.logic.parser.ArgumentMultimap; +import peoplesoft.logic.parser.ArgumentTokenizer; +import peoplesoft.logic.parser.Parser; +import peoplesoft.logic.parser.ParserUtil; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.tag.Tag; /** - * Parses input arguments and creates a new EditCommand object + * Parses input arguments and creates a new PersonEditCommand object */ -public class EditCommandParser implements Parser { +public class PersonEditCommandParser 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 PersonEditCommand + * and returns an PersonEditCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ - public EditCommand parse(String args) throws ParseException { + public PersonEditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_RATE, 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); + throw new ParseException(String.format(MSG_INVALID_CMD_FORMAT, + PersonEditCommand.MESSAGE_USAGE), pe); } EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -55,13 +62,16 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } + if (argMultimap.getValue(PREFIX_RATE).isPresent()) { + editPersonDescriptor.setRate(ParserUtil.parseRate(argMultimap.getValue(PREFIX_RATE).get())); + } parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + throw new ParseException(PersonEditCommand.MESSAGE_NOT_EDITED); } - return new EditCommand(index, editPersonDescriptor); + return new PersonEditCommand(index, editPersonDescriptor); } /** diff --git a/src/main/java/peoplesoft/logic/parser/person/PersonFindCommandParser.java b/src/main/java/peoplesoft/logic/parser/person/PersonFindCommandParser.java new file mode 100644 index 00000000000..252acbe3ec5 --- /dev/null +++ b/src/main/java/peoplesoft/logic/parser/person/PersonFindCommandParser.java @@ -0,0 +1,37 @@ +package peoplesoft.logic.parser.person; + +import static peoplesoft.commons.core.Messages.MSG_INVALID_CMD_FORMAT; + +import java.util.Arrays; + +import peoplesoft.logic.commands.person.PersonFindCommand; +import peoplesoft.logic.parser.Parser; +import peoplesoft.logic.parser.ParserUtil; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.person.PersonContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new PersonFindCommand object + */ +public class PersonFindCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the PersonFindCommand + * and returns a PersonFindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public PersonFindCommand parse(String args) throws ParseException { + String trimmedArgs; + try { + trimmedArgs = ParserUtil.parseString(args); + } catch (ParseException pe) { + throw new ParseException( + String.format(MSG_INVALID_CMD_FORMAT, PersonFindCommand.MESSAGE_USAGE), pe); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + + return new PersonFindCommand(new PersonContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + } + +} diff --git a/src/main/java/peoplesoft/model/AddressBook.java b/src/main/java/peoplesoft/model/AddressBook.java new file mode 100644 index 00000000000..39f6ea3e615 --- /dev/null +++ b/src/main/java/peoplesoft/model/AddressBook.java @@ -0,0 +1,356 @@ +package peoplesoft.model; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.function.UnaryOperator; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import javafx.collections.ObservableList; +import peoplesoft.commons.core.JobIdFactory; +import peoplesoft.commons.core.PersonIdFactory; +import peoplesoft.commons.util.JsonUtil; +import peoplesoft.model.employment.Employment; +import peoplesoft.model.job.Job; +import peoplesoft.model.job.JobList; +import peoplesoft.model.job.UniqueJobList; +import peoplesoft.model.job.exceptions.JobNotFoundException; +import peoplesoft.model.person.Person; +import peoplesoft.model.person.UniquePersonList; +import peoplesoft.model.person.exceptions.PersonNotFoundException; +import peoplesoft.model.util.ID; + +/** + * Wraps all data at the address-book level + * Duplicates are not allowed (by .isSamePerson comparison) + */ +@JsonSerialize(using = AddressBook.AddressBookSerializer.class) +@JsonDeserialize(using = AddressBook.AddressBookDeserializer.class) +public class AddressBook implements ReadOnlyAddressBook { + + private final UniquePersonList persons; + private JobList jobs; + + /** + * Creates an empty AddressBook. + */ + public AddressBook() { + persons = new UniquePersonList(); + jobs = new UniqueJobList(); + } + + /** + * Creates an AddressBook using the Persons in the {@code toBeCopied}. + */ + public AddressBook(ReadOnlyAddressBook toBeCopied) { + this(); + resetData(toBeCopied); + } + + /** + * Creates an {@code AddressBook} using the given {@code UniquePersonList} and {@code UniqueJobList}. + * Only used by {@code AddressBookDeserializer}. + * + * @param upl the {@code UniquePersonList} for the new instance + * @param ujl the {@code UniqueJobList} for the new instance + */ + private AddressBook(UniquePersonList upl, UniqueJobList ujl) { + persons = upl; + jobs = ujl; + } + + //// 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); + } + + /** + * Replaces the contents of the job list with {@code jobs}. + * {@code jobs} must not contain duplicate jobs. + */ + public void setJobs(List jobs) { + this.jobs.setJobs(jobs); + } + + /** + * Resets the existing data of this {@code AddressBook} with {@code newData}. + */ + public void resetData(ReadOnlyAddressBook newData) { + requireNonNull(newData); + + setPersons(newData.getPersonList()); + setJobs(newData.getJobList()); + } + + //// person-level operations + + /** + * Returns true if a person with the given data fields exists in the address book. + */ + public boolean hasPerson(Person person) { + requireNonNull(person); + return persons.contains(person); + } + + /** + * Returns true if a person with the given id exists in the address book. + */ + public boolean hasPerson(ID personId) { + requireNonNull(personId); + return persons.contains(personId); + } + + /** + * Returns the person with the given id. + * + * @throws PersonNotFoundException if there is no such person + */ + public Person getPerson(ID personId) throws PersonNotFoundException { + requireNonNull(personId); + return persons.get(personId); + } + + /** + * 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); + } + + //// job-level operations + + /** + * Returns true if a job with the same identity as {@code job} exists in the address book. + */ + public boolean hasJob(ID jobId) { + requireNonNull(jobId); + return jobs.contains(jobId); + } + + /** + * Returns the job with the given id. + * + * @throws JobNotFoundException if there is no such job + */ + public Job getJob(ID jobId) throws JobNotFoundException { + requireNonNull(jobId); + return jobs.get(jobId); + } + + /** + * Adds a job to the address book. + * The job must not already exist in the address book. + */ + public void addJob(Job job) { + jobs.add(job); + } + + /** + * Replaces the given job {@code target} in the list with {@code editedJob}. + * {@code target} must exist in the address book. + * The job identity of {@code editedJob} must not be the same as another existing job in the address book. + */ + public void setJob(Job target, Job editedJob) { + requireNonNull(editedJob); + + jobs.setJob(target, editedJob); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the address book. + */ + public void removeJob(Job key) { + jobs.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 ObservableList getJobList() { + return jobs.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) + && jobs.equals(((AddressBook) other).jobs)); + } + + @Override + public int hashCode() { + return Objects.hash(persons, jobs); + } + + protected static class AddressBookSerializer extends StdSerializer { + private AddressBookSerializer(Class val) { + super(val); + } + + private AddressBookSerializer() { + this(null); + } + + @Override + public void serialize(AddressBook val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartObject(); + + gen.writeObjectField("persons", val.persons); + gen.writeObjectField("jobs", val.jobs); + gen.writeObjectField("employment", Employment.getInstance()); + gen.writeNumberField("jobIdState", JobIdFactory.getId()); + gen.writeNumberField("personIdState", PersonIdFactory.getId()); + + gen.writeEndObject(); + } + } + + protected static class AddressBookDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The address book is invalid or missing!"; + private static final UnaryOperator INVALID_VAL_FMTR = + k -> String.format("This address book's %s value is invalid!", k); + + private AddressBookDeserializer(Class vc) { + super(vc); + } + + private AddressBookDeserializer() { + this(null); + } + + private static JsonNode getNonNullNode(ObjectNode node, String key, DeserializationContext ctx) + throws JsonMappingException { + return JsonUtil.getNonNullNode(node, key, ctx, INVALID_VAL_FMTR); + } + + private static T getNonNullNodeWithType(ObjectNode node, String key, DeserializationContext ctx, + Class cls) throws JsonMappingException { + return JsonUtil.getNonNullNodeWithType(node, key, ctx, + INVALID_VAL_FMTR, cls); + } + + @Override + public AddressBook deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + ObjectCodec codec = p.getCodec(); + + if (!(node instanceof ObjectNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + ObjectNode objNode = (ObjectNode) node; + + UniquePersonList upl = getNonNullNode(objNode, "persons", ctx) + .traverse(codec) + .readValueAs(UniquePersonList.class); + + UniqueJobList ujl = getNonNullNode(objNode, "jobs", ctx) + .traverse(codec) + .readValueAs(UniqueJobList.class); + + if (objNode.has("employment")) { + Employment emp = objNode.get("employment") // not null, we're good + .traverse(codec) + .readValueAs(Employment.class); + + Employment.setInstance(emp); + } else { + Employment.newInstance(); + } + + if (objNode.has("jobIdState")) { + // note jobId cannot be negative + int jobId = Math.max( + getNonNullNodeWithType(objNode, "jobIdState", ctx, IntNode.class).intValue(), + 0); + + // just in case we get a jobId that already exists + while (ujl.contains(new ID(jobId))) { + jobId++; + } + + JobIdFactory.setId(jobId); + } else { + JobIdFactory.setId(0); + } + + if (objNode.has("personIdState")) { + // note personId cannot be negative + int personId = Math.max( + getNonNullNodeWithType(objNode, "personIdState", ctx, IntNode.class).intValue(), + 0); + + // just in case we get a personId that already exists + while (upl.contains(new ID(personId))) { + personId++; + } + + PersonIdFactory.setId(personId); + } else { + PersonIdFactory.setId(0); + } + + return new AddressBook(upl, ujl); + } + + @Override + public AddressBook getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/peoplesoft/model/Model.java similarity index 52% rename from src/main/java/seedu/address/model/Model.java rename to src/main/java/peoplesoft/model/Model.java index d54df471c1f..cf41932920d 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/peoplesoft/model/Model.java @@ -1,11 +1,15 @@ -package seedu.address.model; +package peoplesoft.model; import java.nio.file.Path; import java.util.function.Predicate; import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; +import peoplesoft.commons.core.GuiSettings; +import peoplesoft.model.job.Job; +import peoplesoft.model.job.exceptions.JobNotFoundException; +import peoplesoft.model.person.Person; +import peoplesoft.model.person.exceptions.PersonNotFoundException; +import peoplesoft.model.util.ID; /** * The API of the Model component. @@ -14,6 +18,9 @@ public interface Model { /** {@code Predicate} that always evaluate to true */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_JOBS = unused -> true; + /** * Replaces user prefs data with the data in {@code userPrefs}. */ @@ -57,6 +64,18 @@ public interface Model { */ boolean hasPerson(Person person); + /** + * Returns true if a person with the given id exists in the address book. + */ + boolean hasPerson(ID personId); + + /** + * Returns the person with the given id if it exists. + * + * @throws PersonNotFoundException if no such person exists + */ + Person getPerson(ID personId); + /** * Deletes the given person. * The person must exist in the address book. @@ -84,4 +103,49 @@ public interface Model { * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Returns true if a job with the same identity as {@code job} exists in the address book. + */ + boolean hasJob(Job job); + + /** + * Returns true if a job with the given id exists in the address book. + */ + boolean hasJob(ID jobId); + + /** + * Returns the job with the given id if it exists. + * + * @throws JobNotFoundException if no such job exists + */ + Job getJob(ID jobId); + + /** + * Deletes the given job. + * The job must exist in the address book. + */ + void deleteJob(Job target); + + /** + * Adds the given job. + * {@code job} must not already exist in the address book. + */ + void addJob(Job job); + + /** + * Replaces the given job {@code target} with {@code editedJob}. + * {@code target} must exist in the address book. + * The job identity of {@code editedJob} must not be the same as another existing job in the address book. + */ + void setJob(Job target, Job editedJob); + + /** Returns an unmodifiable view of the filtered job list */ + ObservableList getFilteredJobList(); + + /** + * Updates the filter of the filtered job list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredJobList(Predicate predicate); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/peoplesoft/model/ModelManager.java similarity index 60% rename from src/main/java/seedu/address/model/ModelManager.java rename to src/main/java/peoplesoft/model/ModelManager.java index 86c1df298d7..c44331481c5 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/peoplesoft/model/ModelManager.java @@ -1,17 +1,22 @@ -package seedu.address.model; +package peoplesoft.model; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Objects; import java.util.function.Predicate; import java.util.logging.Logger; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; +import peoplesoft.commons.core.GuiSettings; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.model.job.Job; +import peoplesoft.model.job.exceptions.JobNotFoundException; +import peoplesoft.model.person.Person; +import peoplesoft.model.person.exceptions.PersonNotFoundException; +import peoplesoft.model.util.ID; /** * Represents the in-memory model of the address book data. @@ -22,6 +27,7 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final FilteredList filteredJobs; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -34,6 +40,7 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredJobs = new FilteredList<>(this.addressBook.getJobList()); } public ModelManager() { @@ -87,12 +94,26 @@ public ReadOnlyAddressBook getAddressBook() { return addressBook; } + //=========== Person Operations ========================================================================== + @Override public boolean hasPerson(Person person) { requireNonNull(person); return addressBook.hasPerson(person); } + @Override + public boolean hasPerson(ID personId) { + requireNonNull(personId); + return addressBook.hasPerson(personId); + } + + @Override + public Person getPerson(ID personId) throws PersonNotFoundException { + requireNonNull(personId); + return addressBook.getPerson(personId); + } + @Override public void deletePerson(Person target) { addressBook.removePerson(target); @@ -107,7 +128,6 @@ public void addPerson(Person person) { @Override public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); - addressBook.setPerson(target, editedPerson); } @@ -128,6 +148,66 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } + //=========== Job Operations ============================================================================= + + @Override + public boolean hasJob(Job job) { + requireNonNull(job); + return hasPerson(job.getJobId()); + } + + @Override + public boolean hasJob(ID jobId) { + requireNonNull(jobId); + return addressBook.hasJob(jobId); + } + + @Override + public Job getJob(ID jobId) throws JobNotFoundException { + requireNonNull(jobId); + return addressBook.getJob(jobId); + } + + @Override + public void deleteJob(Job target) { + addressBook.removeJob(target); + } + + @Override + public void addJob(Job job) { + addressBook.addJob(job); + updateFilteredJobList(PREDICATE_SHOW_ALL_JOBS); + } + + @Override + public void setJob(Job target, Job editedJob) { + requireAllNonNull(target, editedJob); + addressBook.setJob(target, editedJob); + } + + //=========== Filtered Job List Accessors ================================================================ + + /** + * Returns an unmodifiable view of the list of {@code Job} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList getFilteredJobList() { + return filteredJobs; + } + + @Override + public void updateFilteredJobList(Predicate predicate) { + requireNonNull(predicate); + filteredJobs.setPredicate(predicate); + } + + // good practice to include this when overriding equals() + @Override + public int hashCode() { + return Objects.hash(addressBook, userPrefs, filteredPersons, filteredJobs); + } + @Override public boolean equals(Object obj) { // short circuit if same object @@ -144,7 +224,7 @@ public boolean equals(Object obj) { ModelManager other = (ModelManager) obj; return addressBook.equals(other.addressBook) && userPrefs.equals(other.userPrefs) - && filteredPersons.equals(other.filteredPersons); + && filteredPersons.equals(other.filteredPersons) + && filteredJobs.equals(other.filteredJobs); } - } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/peoplesoft/model/ReadOnlyAddressBook.java similarity index 54% rename from src/main/java/seedu/address/model/ReadOnlyAddressBook.java rename to src/main/java/peoplesoft/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..76baf852f46 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/peoplesoft/model/ReadOnlyAddressBook.java @@ -1,7 +1,8 @@ -package seedu.address.model; +package peoplesoft.model; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; +import peoplesoft.model.job.Job; +import peoplesoft.model.person.Person; /** * Unmodifiable view of an address book @@ -14,4 +15,9 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + /** + * Returns an unmodifiable view of the jobs list. + * This list will not contain any duplicate jobs. + */ + ObservableList getJobList(); } diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/peoplesoft/model/ReadOnlyUserPrefs.java similarity index 70% rename from src/main/java/seedu/address/model/ReadOnlyUserPrefs.java rename to src/main/java/peoplesoft/model/ReadOnlyUserPrefs.java index befd58a4c73..3c1c8e5270e 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/peoplesoft/model/ReadOnlyUserPrefs.java @@ -1,8 +1,8 @@ -package seedu.address.model; +package peoplesoft.model; import java.nio.file.Path; -import seedu.address.commons.core.GuiSettings; +import peoplesoft.commons.core.GuiSettings; /** * Unmodifiable view of user prefs. diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/peoplesoft/model/UserPrefs.java similarity index 96% rename from src/main/java/seedu/address/model/UserPrefs.java rename to src/main/java/peoplesoft/model/UserPrefs.java index 25a5fd6eab9..b28f150b158 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/peoplesoft/model/UserPrefs.java @@ -1,4 +1,4 @@ -package seedu.address.model; +package peoplesoft.model; import static java.util.Objects.requireNonNull; @@ -6,7 +6,7 @@ import java.nio.file.Paths; import java.util.Objects; -import seedu.address.commons.core.GuiSettings; +import peoplesoft.commons.core.GuiSettings; /** * Represents User's preferences. diff --git a/src/main/java/peoplesoft/model/employment/Employment.java b/src/main/java/peoplesoft/model/employment/Employment.java new file mode 100644 index 00000000000..7e2a0bbb0f0 --- /dev/null +++ b/src/main/java/peoplesoft/model/employment/Employment.java @@ -0,0 +1,269 @@ +package peoplesoft.model.employment; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; +import peoplesoft.model.Model; +import peoplesoft.model.employment.exceptions.DuplicateEmploymentException; +import peoplesoft.model.employment.exceptions.EmploymentNotFoundException; +import peoplesoft.model.job.Job; +import peoplesoft.model.person.Person; +import peoplesoft.model.util.ID; + +/** + * Association class to handle assigning {@code Jobs} to {@code Persons}. + */ +@JsonSerialize(using = Employment.EmploymentSerializer.class) +@JsonDeserialize(using = Employment.EmploymentDeserializer.class) +public class Employment { + // TODO deserializing an Employment instance with invalid IDs. + /** + * Singleton instance. + */ + private static Employment instance; + + /** + * Maps {@code JobId} to {@code PersonId}. + */ + private Map> map; + + /** + * Constructor for a new employment. + */ + public Employment() { + map = new HashMap<>(); + } + + /** + * Constructor used for serdes. + * + * @param map Map of values + */ + Employment(Map> map) { + requireNonNull(map); + this.map = new HashMap<>(); + for (Map.Entry> e : map.entrySet()) { + this.map.put(e.getKey(), new TreeSet<>(e.getValue())); + } + } + + /** + * Adds an association of a {@code Job} with a {@code Person}. + * Throws an exception if the association already exists. + * + * @param job Job. + * @param person Person. + * @throws DuplicateEmploymentException Throws if association already exists. + */ + public void associate(Job job, Person person) { + requireAllNonNull(job, person); + if (!map.containsKey(job.getJobId())) { + map.put(job.getJobId(), new TreeSet<>()); + } + // Guaranteed to be non-null and Set handles duplicates + if (!map.get(job.getJobId()).add(person.getPersonId())) { + throw new DuplicateEmploymentException(); + } + } + + /** + * Removes an association of a {@code Job} with a {@code Person}. + * Throws an exception if the association is not found. + * + * @param job Job. + * @param person Person. + * @throws EmploymentNotFoundException Throws if association is not found. + */ + public void disassociate(Job job, Person person) throws EmploymentNotFoundException { + requireAllNonNull(job, person); + // Short-circuit evaluation + if (!map.containsKey(job.getJobId()) || !map.get(job.getJobId()).contains(person.getPersonId())) { + throw new EmploymentNotFoundException(); + } + // Guaranteed to be present + map.get(job.getJobId()).remove(person.getPersonId()); + if (map.get(job.getJobId()).isEmpty()) { + map.remove(job.getJobId()); + } + } + + /** + * Deletes all entries of a {@code Person}. + * + * @param person {@code Person} to delete. + */ + public void deletePerson(Person person) { + requireAllNonNull(person); + for (Map.Entry> e : map.entrySet()) { + e.getValue().removeIf(p -> p.equals(person.getPersonId())); + } + // If list of IDs for persons is empty, remove the entry from the map + map.entrySet().removeIf(e -> e.getValue().isEmpty()); + } + + /** + * Deletes all entries of a {@code Job}. + * + * @param job {@code Job} to delete. + */ + public void deleteJob(Job job) { + requireAllNonNull(job); + map.remove(job.getJobId()); + } + + /** + * Returns a list of {@code Jobs} that a {@code Person} has. + * Also updates the FilteredJobList for UI. + * + * @param person Person. + * @param model Model. + * @return List of jobs. + */ + public List getJobs(Person person, Model model) { + requireAllNonNull(person, model); + + List jobs = new ArrayList<>(); + + for (Map.Entry> entry : map.entrySet()) { + if (entry.getValue().contains(person.getPersonId())) { + jobs.add(model.getJob(entry.getKey())); + } + } + + return jobs; + } + + /** + * Returns a list of {@code Person}s assigned to a {@code Job}. + * Also updates the FilteredPersonList for UI. + * + * @param job Job. + * @param model Model. + * @return List of persons. + */ + public List getPersons(Job job, Model model) { + requireAllNonNull(job, model); + + if (!map.containsKey(job.getJobId())) { + return List.of(); + } else { + return map.get(job.getJobId()).stream() + .map(model::getPerson) + .collect(Collectors.toList()); + } + } + + /** + * Returns a list of {@code Persons} assigned to a {@code Job}. + * + * @return Map of jobs. + */ + public Map> getAllJobs() { + return map; + } + + /** + * Sets the singleton instance of {@code Employment}. + * + * @param employment Instance to set. + */ + public static void setInstance(Employment employment) { + requireNonNull(employment); + instance = employment; + } + + /** + * Creates a new singleton instance of {@code Employment}. + * + */ + public static void newInstance() { + instance = new Employment(); + } + + /** + * Returns the instance of {@code Employment}. + * + * @return {@code Employment} instance. + */ + public static Employment getInstance() { + if (instance == null) { + instance = new Employment(); + } + return instance; + } + + protected static class EmploymentSerializer extends StdSerializer { + private EmploymentSerializer(Class val) { + super(val); + } + + private EmploymentSerializer() { + this(null); + } + + @Override + public void serialize(Employment value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeObject(value.map); + } + } + + protected static class EmploymentDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "Invalid employment!"; + + private EmploymentDeserializer(Class vc) { + super(vc); + } + + private EmploymentDeserializer() { + this(null); + } + + @Override + public Employment deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + ObjectCodec codec = p.getCodec(); + + if (!(node instanceof ObjectNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + // readValueAs Map is ok because we know `node` has to be a json object + Map> map = node + .traverse(codec) + .readValueAs(new TypeReference>>(){}); + + return new Employment(map); + } + + @Override + public Employment getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/employment/exceptions/DuplicateEmploymentException.java b/src/main/java/peoplesoft/model/employment/exceptions/DuplicateEmploymentException.java new file mode 100644 index 00000000000..2e20f4676d8 --- /dev/null +++ b/src/main/java/peoplesoft/model/employment/exceptions/DuplicateEmploymentException.java @@ -0,0 +1,4 @@ +package peoplesoft.model.employment.exceptions; + +public class DuplicateEmploymentException extends RuntimeException { +} diff --git a/src/main/java/peoplesoft/model/employment/exceptions/EmploymentNotFoundException.java b/src/main/java/peoplesoft/model/employment/exceptions/EmploymentNotFoundException.java new file mode 100644 index 00000000000..4f96001354e --- /dev/null +++ b/src/main/java/peoplesoft/model/employment/exceptions/EmploymentNotFoundException.java @@ -0,0 +1,4 @@ +package peoplesoft.model.employment.exceptions; + +public class EmploymentNotFoundException extends RuntimeException { +} diff --git a/src/main/java/peoplesoft/model/job/Job.java b/src/main/java/peoplesoft/model/job/Job.java new file mode 100644 index 00000000000..8b2cb605115 --- /dev/null +++ b/src/main/java/peoplesoft/model/job/Job.java @@ -0,0 +1,284 @@ +package peoplesoft.model.job; + +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; + +import java.io.IOException; +import java.time.Duration; +import java.util.Objects; +import java.util.function.UnaryOperator; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; +import peoplesoft.model.job.exceptions.JobNotPaidException; +import peoplesoft.model.job.exceptions.ModifyFinalizedJobException; +import peoplesoft.model.money.Money; +import peoplesoft.model.money.Rate; +import peoplesoft.model.util.ID; + +/** + * Represents a job. Immutable. + */ +@JsonSerialize(using = Job.JobSerializer.class) +@JsonDeserialize(using = Job.JobDeserializer.class) +public class Job { + + private final ID jobId; + private final String desc; + private final Duration duration; + + private final boolean hasPaid; + private final boolean isFinal; + + /** + * Constructor for an immutable job. + * All fields must not be null. + */ + public Job(ID jobId, String desc, Duration duration) { + requireAllNonNull(jobId, desc, duration); + this.jobId = jobId; + this.desc = desc; + this.duration = duration; + this.hasPaid = false; + this.isFinal = false; + } + + /** + * Constructor to set all fields. + */ + public Job(ID jobId, String desc, Duration duration, boolean hasPaid, boolean isFinal) { + requireAllNonNull(jobId, desc, duration, hasPaid, isFinal); + this.jobId = jobId; + this.desc = desc; + this.duration = duration; + this.hasPaid = hasPaid; + this.isFinal = isFinal; + } + + public ID getJobId() { + return jobId; + } + + public String getDesc() { + return desc; + } + + public Duration getDuration() { + return duration; + } + + public boolean hasPaid() { + return hasPaid; + } + + // TODO: Not sure if checks should be within Job or within whatever uses Job + public boolean isFinal() { + return isFinal; + } + + /** + * Returns the pay of the job. + * Calculated from rate and duration. + * + * @param rate Rate of job. + * @return Pay. + */ + public Money calculatePay(Rate rate) { + return rate.calculateAmount(duration); + } + + /** + * Returns a new instance of the job with isPaid as true; + * + * @return Paid job. + * @throws ModifyFinalizedJobException Job should not be modified after it is finalized. + */ + public Job setAsPaid() throws ModifyFinalizedJobException { + if (isFinal) { + throw new ModifyFinalizedJobException(); + } + return new Job(jobId, desc, duration, true, false); + } + + /** + * Returns a new instance of the job with isPaid as false; + * + * @return Unpaid job. + * @throws ModifyFinalizedJobException Job should not be modified after it is finalized. + */ + public Job setAsNotPaid() throws ModifyFinalizedJobException { + if (isFinal) { + throw new ModifyFinalizedJobException(); + } + return new Job(jobId, desc, duration, false, false); + } + + /** + * Finalizes payments of a job. Requires the job to be paid. + * A finalized job cannot (and should not) be modified. + * + * @return Finalized job. + * @throws JobNotPaidException If job is not paid. + */ + public Job setAsFinal() throws JobNotPaidException { + if (!hasPaid) { + throw new JobNotPaidException(); + } + return new Job(jobId, desc, duration, true, true); + } + + /** + * Returns true if both jobs have the same {@code jobId}. + * This defines a weaker notion of equality between two jobs. + * + * @param other the other Job to compare against + * @return true if both jobs have the same {@code jobId} + */ + public boolean isSameJob(Job other) { + if (other == this) { + return true; + } + + return other != null + && other.getJobId().equals(getJobId()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Job)) { + return false; + } + Job otherJob = (Job) other; + return otherJob.getJobId().equals(getJobId()) + && otherJob.getDesc().equals(getDesc()) + && otherJob.getDuration().equals(getDuration()) + && otherJob.hasPaid() == hasPaid() + && otherJob.isFinal() == isFinal(); + } + + @Override + public int hashCode() { + return Objects.hash(jobId, desc, duration, hasPaid, isFinal); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("ID: ") + .append(getJobId()) + .append("; Name: ") + .append(getDesc()) + .append("; Duration: ") + .append(getDuration().toHoursPart()) + .append("H") + .append(getDuration().toMinutesPart()) + .append("M") + .append("; Has paid: ") + .append(hasPaid()); + + return builder.toString(); + } + + protected static class JobSerializer extends StdSerializer { + private JobSerializer(Class val) { + super(val); + } + + private JobSerializer() { + this(null); + } + + @Override + public void serialize(Job value, JsonGenerator gen, SerializerProvider provider)throws IOException { + gen.writeStartObject(); + + gen.writeObjectField("jobId", value.getJobId()); + gen.writeStringField("desc", value.getDesc()); + gen.writeObjectField("duration", value.getDuration()); + gen.writeBooleanField("hasPaid", value.hasPaid()); + gen.writeBooleanField("isFinal", value.isFinal()); + + gen.writeEndObject(); + } + } + + protected static class JobDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The job instance is invalid or missing!"; + private static final UnaryOperator INVALID_VAL_FMTR = + k -> String.format("This job's %s value is invalid!", k); + + private JobDeserializer(Class vc) { + super(vc); + } + + private JobDeserializer() { + this(null); + } + + private static JsonNode getNonNullNode(ObjectNode node, String key, DeserializationContext ctx) + throws JsonMappingException { + return JsonUtil.getNonNullNode(node, key, ctx, INVALID_VAL_FMTR); + } + + private static T getNonNullNodeWithType(ObjectNode node, String key, DeserializationContext ctx, + Class cls) throws JsonMappingException { + return JsonUtil.getNonNullNodeWithType(node, key, ctx, + INVALID_VAL_FMTR, cls); + } + + @Override + public Job deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + ObjectCodec codec = p.getCodec(); + + if (!(node instanceof ObjectNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + ObjectNode job = (ObjectNode) node; + + ID jobId = getNonNullNode(job, "jobId", ctx) + .traverse(codec) + .readValueAs(ID.class); + + String desc = getNonNullNodeWithType(job, "desc", ctx, TextNode.class).textValue(); + + Duration duration = getNonNullNode(job, "duration", ctx) + .traverse(codec) + .readValueAs(Duration.class); + + if (duration.isNegative() || duration.isZero() || duration.toMinutes() > 60000000) { + throw JsonUtil.getWrappedIllegalValueException(ctx, INVALID_VAL_FMTR.apply("duration")); + } + + Boolean hasPaid = getNonNullNodeWithType(job, "hasPaid", ctx, BooleanNode.class).booleanValue(); + + Boolean isFinal = getNonNullNodeWithType(job, "isFinal", ctx, BooleanNode.class).booleanValue(); + + return new Job(jobId, desc, duration, hasPaid, isFinal); + } + + @Override + public Job getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/job/JobContainsKeywordsPredicate.java b/src/main/java/peoplesoft/model/job/JobContainsKeywordsPredicate.java new file mode 100644 index 00000000000..b51855153d5 --- /dev/null +++ b/src/main/java/peoplesoft/model/job/JobContainsKeywordsPredicate.java @@ -0,0 +1,28 @@ +package peoplesoft.model.job; + +import java.util.List; +import java.util.function.Predicate; + +import peoplesoft.commons.util.StringUtil; + +public class JobContainsKeywordsPredicate implements Predicate { + + private final List keywords; + + public JobContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Job job) { + return !keywords.isEmpty() && keywords.stream() + .allMatch(keyword -> StringUtil.containsWordIgnoreCase(job.getDesc(), keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof JobContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((JobContainsKeywordsPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/peoplesoft/model/job/JobList.java b/src/main/java/peoplesoft/model/job/JobList.java new file mode 100644 index 00000000000..860c2f5aa9a --- /dev/null +++ b/src/main/java/peoplesoft/model/job/JobList.java @@ -0,0 +1,30 @@ +package peoplesoft.model.job; + +import java.util.List; + +import javafx.collections.ObservableList; +import peoplesoft.model.job.exceptions.JobNotFoundException; +import peoplesoft.model.util.ID; + +public interface JobList extends Iterable { + + // TODO: should jobs compare by jobs or jobId, since add it seems more intuitive + // to compare by jobs but delete seems more intuitive to compare by jobId. + // For now it is jobId. + boolean contains(ID jobId); + + Job get(ID jobId) throws JobNotFoundException; + + void add(Job toAdd); + + void remove(Job toRemove); + + void setJob(Job targetJob, Job editedJob); + + void setJobs(List jobs); + + ObservableList asUnmodifiableObservableList(); + + boolean jobsAreUnique(List jobs); + +} diff --git a/src/main/java/peoplesoft/model/job/UniqueJobList.java b/src/main/java/peoplesoft/model/job/UniqueJobList.java new file mode 100644 index 00000000000..674e525af86 --- /dev/null +++ b/src/main/java/peoplesoft/model/job/UniqueJobList.java @@ -0,0 +1,201 @@ +package peoplesoft.model.job; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import peoplesoft.commons.util.JsonUtil; +import peoplesoft.model.job.exceptions.DuplicateJobException; +import peoplesoft.model.job.exceptions.JobNotFoundException; +import peoplesoft.model.util.ID; + +/** + * Implementation of {@code JobList}. + */ +@JsonSerialize(using = UniqueJobList.UniqueJobListSerializer.class) +@JsonDeserialize(using = UniqueJobList.UniqueJobListDeserializer.class) +public class UniqueJobList implements JobList { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + @Override + public boolean contains(ID jobId) { + requireNonNull(jobId); + return internalList.stream().anyMatch(job -> job.getJobId().equals(jobId)); + } + + /** + * Returns the job with the given id. + * + * @throws JobNotFoundException if the person does not exist + */ + @Override + public Job get(ID jobId) throws JobNotFoundException { + requireNonNull(jobId); + return internalList.stream() + .filter(j -> j != null && jobId.equals(j.getJobId())) + .findAny() + .orElseThrow(JobNotFoundException::new); + } + + @Override + public void add(Job toAdd) { + requireNonNull(toAdd); + if (contains(toAdd.getJobId())) { + throw new DuplicateJobException(); + } + internalList.add(toAdd); + } + + @Override + public void remove(Job toRemove) { + requireNonNull(toRemove); + if (!contains(toRemove.getJobId())) { + throw new JobNotFoundException(); + } + internalList.removeIf(job -> job.isSameJob(toRemove)); + } + + @Override + public void setJob(Job targetJob, Job editedJob) { + requireAllNonNull(targetJob, editedJob); + + int index = internalList.indexOf(targetJob); + if (index == -1) { + throw new JobNotFoundException(); + } + + if (!targetJob.isSameJob(editedJob) && contains(editedJob.getJobId())) { + throw new DuplicateJobException(); + } + + internalList.set(index, editedJob); + } + + public void setJobs(UniqueJobList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + @Override + public void setJobs(List jobs) { + requireAllNonNull(jobs); + if (!jobsAreUnique(jobs)) { + throw new DuplicateJobException(); + } + internalList.setAll(jobs); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + /** + * Returns true if {@code jobs} contains only unique persons. + */ + @Override + public boolean jobsAreUnique(List jobs) { + for (int i = 0; i < jobs.size() - 1; i++) { + for (int j = i + 1; j < jobs.size(); j++) { + if (jobs.get(i).isSameJob(jobs.get(j))) { + return false; + } + } + } + return true; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueJobList // instanceof handles nulls + && internalList.equals(((UniqueJobList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + protected static class UniqueJobListSerializer extends StdSerializer { + private UniqueJobListSerializer(Class val) { + super(val); + } + + private UniqueJobListSerializer() { + this(null); + } + + @Override + public void serialize(UniqueJobList val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeObject(val.asUnmodifiableObservableList()); + } + } + + protected static class UniqueJobListDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The job list is invalid or missing!"; + + private UniqueJobListDeserializer(Class vc) { + super(vc); + } + + private UniqueJobListDeserializer() { + this(null); + } + + @Override + public UniqueJobList deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + ObjectCodec codec = p.getCodec(); + + if (!(node instanceof ArrayNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + List jobList = node // is ArrayNode + .traverse(codec) + .readValueAs(new TypeReference>(){}); + + UniqueJobList ujl = new UniqueJobList(); + ujl.setJobs(jobList); + + return ujl; + } + + @Override + public UniqueJobList getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/job/exceptions/DuplicateJobException.java b/src/main/java/peoplesoft/model/job/exceptions/DuplicateJobException.java new file mode 100644 index 00000000000..576b2aec9dd --- /dev/null +++ b/src/main/java/peoplesoft/model/job/exceptions/DuplicateJobException.java @@ -0,0 +1,11 @@ +package peoplesoft.model.job.exceptions; + +/** + * Signals that the operation will result in duplicate Jobs (Jobs are considered duplicates if they have the same + * identity). + */ +public class DuplicateJobException extends RuntimeException { + public DuplicateJobException() { + super("Operation would result in duplicate jobs"); + } +} diff --git a/src/main/java/peoplesoft/model/job/exceptions/JobNotFoundException.java b/src/main/java/peoplesoft/model/job/exceptions/JobNotFoundException.java new file mode 100644 index 00000000000..a92cbd4aa1d --- /dev/null +++ b/src/main/java/peoplesoft/model/job/exceptions/JobNotFoundException.java @@ -0,0 +1,6 @@ +package peoplesoft.model.job.exceptions; + +/** + * Signals that the operation is unable to find the specified job. + */ +public class JobNotFoundException extends RuntimeException {} diff --git a/src/main/java/peoplesoft/model/job/exceptions/JobNotPaidException.java b/src/main/java/peoplesoft/model/job/exceptions/JobNotPaidException.java new file mode 100644 index 00000000000..9ed7cb540ed --- /dev/null +++ b/src/main/java/peoplesoft/model/job/exceptions/JobNotPaidException.java @@ -0,0 +1,6 @@ +package peoplesoft.model.job.exceptions; + +/** + * Signals that the Job cannot be finalized as it is not paid. + */ +public class JobNotPaidException extends RuntimeException {} diff --git a/src/main/java/peoplesoft/model/job/exceptions/ModifyFinalizedJobException.java b/src/main/java/peoplesoft/model/job/exceptions/ModifyFinalizedJobException.java new file mode 100644 index 00000000000..41e6cfa6248 --- /dev/null +++ b/src/main/java/peoplesoft/model/job/exceptions/ModifyFinalizedJobException.java @@ -0,0 +1,10 @@ +package peoplesoft.model.job.exceptions; + +/** + * Signifies that a finalized Job was attempted to be modified. + */ +public class ModifyFinalizedJobException extends RuntimeException { + public ModifyFinalizedJobException() { + super("Cannot modify a job that has finalized payment."); + } +} diff --git a/src/main/java/peoplesoft/model/job/exceptions/NonPositiveDurationException.java b/src/main/java/peoplesoft/model/job/exceptions/NonPositiveDurationException.java new file mode 100644 index 00000000000..1d0532fb40b --- /dev/null +++ b/src/main/java/peoplesoft/model/job/exceptions/NonPositiveDurationException.java @@ -0,0 +1,10 @@ +package peoplesoft.model.job.exceptions; + +/** + * Signals that the operation will result in non-positive duration. + */ +public class NonPositiveDurationException extends RuntimeException { + public NonPositiveDurationException() { + super("Operation would result in duration with non-positive value"); + } +} diff --git a/src/main/java/peoplesoft/model/job/util/DurationUtil.java b/src/main/java/peoplesoft/model/job/util/DurationUtil.java new file mode 100644 index 00000000000..ddd24c224da --- /dev/null +++ b/src/main/java/peoplesoft/model/job/util/DurationUtil.java @@ -0,0 +1,19 @@ +package peoplesoft.model.job.util; + +import java.time.Duration; + +import peoplesoft.model.job.exceptions.NonPositiveDurationException; + +/** + * Contains utility method for validating the value of {@code Duration}. + */ +public class DurationUtil { + /** + * Throws NonPositiveDurationException if {@code duration} is not positive. + */ + public static void requirePositive(Duration duration) { + if (duration.isZero() || duration.isNegative()) { + throw new NonPositiveDurationException(); + } + } +} diff --git a/src/main/java/peoplesoft/model/job/util/MoneyUtil.java b/src/main/java/peoplesoft/model/job/util/MoneyUtil.java new file mode 100644 index 00000000000..0758a389741 --- /dev/null +++ b/src/main/java/peoplesoft/model/job/util/MoneyUtil.java @@ -0,0 +1,18 @@ +package peoplesoft.model.job.util; + +import peoplesoft.model.money.Money; +import peoplesoft.model.money.exceptions.NegativeMoneyValueException; + +/** + * Contains utility method for validating the value of {@code Money}. + */ +public class MoneyUtil { + /** + * Throws NegativeMoneyValueException if {@code money} is negative. + */ + public static void requireNonNegative(Money money) { + if (money.getValue().signum() < 0) { + throw new NegativeMoneyValueException(); + } + } +} diff --git a/src/main/java/peoplesoft/model/money/Money.java b/src/main/java/peoplesoft/model/money/Money.java new file mode 100644 index 00000000000..905734f0ba3 --- /dev/null +++ b/src/main/java/peoplesoft/model/money/Money.java @@ -0,0 +1,228 @@ +package peoplesoft.model.money; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.util.Locale; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; + +/** + * Represents some value of money. Immutable. + */ +@JsonSerialize(using = Money.MoneySerializer.class) +@JsonDeserialize(using = Money.MoneyDeserializer.class) +public class Money { + + private static final int VALUE_SCALE = 6; + private static final NumberFormat CURRENCY_FORMAT = NumberFormat.getCurrencyInstance(Locale.US); + + public final BigDecimal value; + + /** + * Constructs a {@code Money}. + * + * @param value A value as a double. + */ + public Money(double value) { + this(BigDecimal.valueOf(value)); + } + + /** + * Constructs a {@code Money}. + * + * @param value A value as a BigDecimal. + */ + public Money(BigDecimal value) { + requireNonNull(value); + CURRENCY_FORMAT.setRoundingMode(RoundingMode.HALF_UP); + this.value = value.setScale(VALUE_SCALE, RoundingMode.HALF_UP); + } + + /** + * Returns true if a given string is a valid money string. + * + * TODO: should we accept strings with {@code $}, {@code ,}, etc? + */ + public static boolean isValidMoneyString(String moneyString) { + try { + new BigDecimal(moneyString); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + public BigDecimal getValue() { + return value; + } + + /** + * Returns a {@code Money} with the value equal the sum of both values. + * + * @param augend Value to add. + * @return Sum. + */ + public Money add(Money augend) { + return new Money(value.add(augend.value).setScale(VALUE_SCALE, RoundingMode.HALF_UP)); + } + + /** + * Returns a {@code Money} with the value equal the sum of both values. + * + * @param augend Value to add. + * @return Sum. + */ + public Money add(BigDecimal augend) { + return new Money(value.add(augend).setScale(VALUE_SCALE, RoundingMode.HALF_UP)); + } + + /** + * Returns a {@code Money} with the value equal the difference of the second value from the first. + * + * @param augend Value to subtract. + * @return Difference. + */ + public Money subtract(Money augend) { + return new Money(value.add(augend.value.negate()).setScale(VALUE_SCALE, RoundingMode.HALF_UP)); + } + + /** + * Returns a {@code Money} with the value equal the product of both values. + * + * @param multiplicand Value to multiply. + * @return Product. + */ + public Money multiply(BigDecimal multiplicand) { + return new Money(value.multiply(multiplicand).setScale(VALUE_SCALE, RoundingMode.HALF_UP)); + } + + /** + * Returns a {@code Money} with the value equal the product of both values. + * + * @param multiplicand Value to multiply. + * @return Product. + */ + public Money multiply(Money multiplicand) { + return new Money(value.multiply(multiplicand.value).setScale(VALUE_SCALE, RoundingMode.HALF_UP)); + } + + /** + * Returns a {@code Money} with the value equal the quotient of the first value over the second. + * + * @param divisor Value to divide. + * @return Quotient. + */ + public Money divide(BigDecimal divisor) { + return new Money(value.divide(divisor, VALUE_SCALE, RoundingMode.HALF_UP)); + } + + /** + * Returns a {@code Money} with the value equal the quotient of the first value over the second. + * + * @param divisor Value to divide. + * @return Quotient. + */ + public Money divide(Money divisor) { + return new Money(value.divide(divisor.value, VALUE_SCALE, RoundingMode.HALF_UP)); + } + + /** + * Prints the 6 decimal place representation of the value. + * + * @return Value as a string. + */ + public String printFullValue() { + return value.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Money // instanceof handles nulls + && value.equals(((Money) other).value)); // state check + } + + @Override + public int hashCode() { + // Might change if scale not matching is an issue + return value.hashCode(); + } + + /** + * Prints the 2 decimal place currency format of the value. + * + * @returns Value in currency format as a string. + */ + @Override + public String toString() { + // Might change if scale not matching is an issue + return CURRENCY_FORMAT.format(value); + } + + protected static class MoneySerializer extends StdSerializer { + private MoneySerializer(Class val) { + super(val); + } + + private MoneySerializer() { + this(null); + } + + @Override + public void serialize(Money val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(val.value.toString()); // to preserve precision + } + } + + protected static class MoneyDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The money value is invalid or missing!"; + + private MoneyDeserializer(Class vc) { + super(vc); + } + + private MoneyDeserializer() { + this(null); + } + + @Override + public Money deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + + if (!(node instanceof TextNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + String valString = ((TextNode) node).textValue(); + + if (!Money.isValidMoneyString(valString)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + return new Money(new BigDecimal(valString)); + } + + @Override + public Money getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/money/Payment.java b/src/main/java/peoplesoft/model/money/Payment.java new file mode 100644 index 00000000000..5f4aa0b0e26 --- /dev/null +++ b/src/main/java/peoplesoft/model/money/Payment.java @@ -0,0 +1,308 @@ +package peoplesoft.model.money; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; + +import java.io.IOException; +import java.util.Objects; +import java.util.function.UnaryOperator; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; +import peoplesoft.model.job.Job; +import peoplesoft.model.money.exceptions.PaymentAlreadyPaidException; +import peoplesoft.model.person.Person; +import peoplesoft.model.util.ID; + +/** + * Represents a Payment for a Person for completing a Job. + * Guarantees: details are present and not null, field values are validated, immutable. + * + * Note that there can only ever be one Payment for each person-job pairing; this will be the unique + * identifier for Payment objects + */ +@JsonSerialize(using = Payment.PaymentSerializer.class) +@JsonDeserialize(using = Payment.PaymentDeserializer.class) +public abstract class Payment { + // Identity fields + private final ID personId; + private final ID jobId; // the job that resulted in the creation of this instance; should already be completed + + // Data fields + private final Money amount; + + private Payment(ID personId, ID jobId, Money amount) { + requireAllNonNull(personId, jobId, amount); + + this.personId = personId; + this.jobId = jobId; + this.amount = amount; + } + + private Payment(Payment payment) { + requireNonNull(payment); + assert payment.personId != null + && payment.jobId != null + && payment.amount != null + : "Payment fields should not be null!"; + + this.personId = payment.personId; + this.jobId = payment.jobId; + this.amount = payment.amount; + } + + /** + * Creates a {@code PendingPayment} object. + * + * @param person Person. + * @param job Job. + * @param amount Amount to pay. + * @return + */ + // TODO: not sure if should be public, but currently public for testing + public static Payment createPayment(Person person, Job job, Money amount) { + requireAllNonNull(person, job, amount); + return new PendingPayment(person.getPersonId(), job.getJobId(), amount); + } + + public ID getPersonId() { + return personId; + } + + public ID getJobId() { + return jobId; + } + + public Money getAmount() { + return amount; + } + + /** + * Returns true if both payments have the person and job fields. + * This defines a weaker notion of equality between two payments. + */ + public boolean isSamePayment(Payment otherPayment) { + if (otherPayment == this) { + return true; + } + + return otherPayment != null + && getPersonId().equals(otherPayment.getPersonId()) + && getJobId().equals(otherPayment.getJobId()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof Payment)) { + return false; + } + + Payment p = (Payment) o; + return isSamePayment(p) && getAmount().equals(p.getAmount()); + } + + @Override + public String toString() { + return String.format( + "Payment for: %s; for job: %s; amount: %s", + personId, jobId, amount); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(personId, jobId, amount); + } + + public abstract boolean isCompleted(); + public abstract Payment pay(); + + private static class CompletedPayment extends Payment { + private CompletedPayment(Payment payment) { + super(payment); + } + + // this one's only used for deser + private CompletedPayment(ID personId, ID jobId, Money money) { + super(personId, jobId, money); + } + + @Override + public boolean isCompleted() { + return true; + } + + @Override + public Payment pay() { + throw new PaymentAlreadyPaidException(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + return o instanceof CompletedPayment && super.equals(o); + } + + @Override + public String toString() { + return super.toString() + "; paid: yes"; + } + } + + private static class PendingPayment extends Payment { + private PendingPayment(ID personId, ID jobId, Money money) { + super(personId, jobId, money); + } + + // PendingPayment(Payment) not included as we probably won't be creating instances that way + + @Override + public boolean isCompleted() { + return false; + } + + @Override + public Payment pay() { + return new CompletedPayment(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + return o instanceof PendingPayment && super.equals(o); + } + + @Override + public String toString() { + return super.toString() + "; paid: no"; + } + } + + protected static class PaymentSerializer extends StdSerializer { + private PaymentSerializer(Class val) { + super(val); + } + + private PaymentSerializer() { + this(null); + } + + @Override + public void serialize(Payment val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartObject(); + + String state = val instanceof PendingPayment + ? "PENDING" + : val instanceof CompletedPayment + ? "COMPLETED" + : null; + + if (state == null) { + throw new IllegalStateException("Unknown payment state!"); + } + + gen.writeStringField("state", state); + // personId association is implicit (since Payment objs are contained within Persons) + gen.writeObjectField("jobId", val.getJobId()); + gen.writeObjectField("amount", val.getAmount()); + + gen.writeEndObject(); + } + } + + protected static class PaymentDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The payment instance is invalid or missing!"; + private static final UnaryOperator INVALID_VAL_FMTR = + k -> String.format("This payment's %s value is invalid!", k); + + private PaymentDeserializer(Class vc) { + super(vc); + } + + private PaymentDeserializer() { + this(null); + } + + private static JsonNode getNonNullNode(ObjectNode node, String key, DeserializationContext ctx) + throws JsonMappingException { + return JsonUtil.getNonNullNode(node, key, ctx, INVALID_VAL_FMTR); + } + + private static T getNonNullNodeWithType(ObjectNode node, String key, DeserializationContext ctx, + Class cls) throws JsonMappingException { + return JsonUtil.getNonNullNodeWithType(node, key, ctx, + INVALID_VAL_FMTR, cls); + } + + @Override + public Payment deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + ObjectCodec codec = p.getCodec(); + + if (!(node instanceof ObjectNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + ObjectNode payment = (ObjectNode) node; + + String state = getNonNullNodeWithType(payment, "state", ctx, TextNode.class) + .textValue(); + + Object personIdAttr = ctx.getAttribute("personId"); + if (!(personIdAttr instanceof ID)) { + // this would be a programming error, not a user error or w/e + // TODO should we still throw a JsonMappingException to recover gracefully? + throw new IllegalStateException("personId not present in deser ctx, or is of wrong type!"); + } + + ID personId = (ID) personIdAttr; + + ID jobId = getNonNullNode(payment, "jobId", ctx) + .traverse(codec) + .readValueAs(ID.class); + + Money amount = getNonNullNode(payment, "amount", ctx) + .traverse(codec) + .readValueAs(Money.class); + + switch (state) { + case "PENDING": + return new PendingPayment(personId, jobId, amount); + case "COMPLETED": + return new CompletedPayment(personId, jobId, amount); + default: + throw JsonUtil.getWrappedIllegalValueException(ctx, INVALID_VAL_FMTR.apply("state")); + } + } + + @Override + public Payment getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/money/PaymentHandler.java b/src/main/java/peoplesoft/model/money/PaymentHandler.java new file mode 100644 index 00000000000..4f956007c0e --- /dev/null +++ b/src/main/java/peoplesoft/model/money/PaymentHandler.java @@ -0,0 +1,94 @@ +package peoplesoft.model.money; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import peoplesoft.model.Model; +import peoplesoft.model.employment.Employment; +import peoplesoft.model.job.Job; +import peoplesoft.model.money.exceptions.PaymentRequiresPersonException; +import peoplesoft.model.person.Person; +import peoplesoft.model.util.ID; + +/** + * Class that handles all the coupling for the creation of {@code Payment} objects. + */ +public class PaymentHandler { + // TODO: reduce time complexity if anyone wants to flex their CS2040S + + /** + * Creates {@code PendingPayment} objects for each {@code Person} assigned + * to a {@code Job}. + * + * @param job Job. + * @param model Model. + * @param emp Employment instance. (Mainly for testing) + * @throws PaymentRequiresPersonException If there is no {@code Person} assigned to the {@code Job}. + */ + public static void createPendingPayments(Job job, Model model, Employment emp) { + assert !job.isFinal(); + List persons = emp.getPersons(job, model); + if (persons.isEmpty()) { + throw new PaymentRequiresPersonException(); + } + for (Person p : persons) { + Map payments = new HashMap<>(p.getPayments()); + Payment newPayment = Payment.createPayment(p, job, job.calculatePay(p.getRate())); + payments.put(job.getJobId(), newPayment); + Person newPerson = new Person(p.getPersonId(), p.getName(), p.getPhone(), p.getEmail(), + p.getAddress(), p.getRate(), p.getTags(), payments); + model.setPerson(p, newPerson); // I hope this does not cause iteration errors + } + } + + /** + * Removes {@code PendingPayment} objects for each {@code Person} assigned + * to a {@code Job}. + * + * @param job Job. + * @param model Model. + * @param emp Employment instance. (Mainly for testing) + * @throws PaymentRequiresPersonException If there is no {@code Person} assigned to the {@code Job}. + */ + public static void removePendingPayments(Job job, Model model, Employment emp) { + assert !job.isFinal(); + // Currently checks all persons, in case there is a user who assigns, marks, un-assigns by editing + // the data file, and un-marks. + model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + List persons = model.getFilteredPersonList(); + if (persons.isEmpty()) { + throw new PaymentRequiresPersonException(); + } + for (Person p : persons) { + Map payments = new HashMap<>(p.getPayments()); + payments.entrySet().removeIf(e -> e.getKey().equals(job.getJobId()) && !e.getValue().isCompleted()); + Person newPerson = new Person(p.getPersonId(), p.getName(), p.getPhone(), p.getEmail(), + p.getAddress(), p.getRate(), p.getTags(), payments); + model.setPerson(p, newPerson); // I hope this does not cause iteration errors + } + } + + /** + * Converts {@code PendingPayment} objects into {@code CompletedPayment} objects. + * + * @param job Job. + * @param model Model. + * @param emp Employment instance. (Mainly for testing) + * @throws PaymentRequiresPersonException If there is no {@code Person} assigned to the {@code Job}. + */ + public static void finalizePayments(Job job, Model model, Employment emp) { + assert job.isFinal(); + List persons = emp.getPersons(job, model); + if (persons.isEmpty()) { + throw new PaymentRequiresPersonException(); + } + for (Person p : persons) { + Map payments = new HashMap<>(p.getPayments()); + payments.replaceAll((id, payment) -> id.equals(job.getJobId()) ? payment.pay() : payment); + Person newPerson = new Person(p.getPersonId(), p.getName(), p.getPhone(), p.getEmail(), + p.getAddress(), p.getRate(), p.getTags(), payments); + model.setPerson(p, newPerson); // I hope this does not cause iteration errors + } + } +} diff --git a/src/main/java/peoplesoft/model/money/Rate.java b/src/main/java/peoplesoft/model/money/Rate.java new file mode 100644 index 00000000000..5ac720e4639 --- /dev/null +++ b/src/main/java/peoplesoft/model/money/Rate.java @@ -0,0 +1,208 @@ +package peoplesoft.model.money; + +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; +import static peoplesoft.model.job.util.DurationUtil.requirePositive; +import static peoplesoft.model.job.util.MoneyUtil.requireNonNegative; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.time.Duration; +import java.time.format.DateTimeParseException; +import java.util.Objects; +import java.util.function.UnaryOperator; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; +import peoplesoft.model.money.exceptions.NegativeMoneyValueException; + +/** + * Represents a rate of payment, e.g. $5 per hour. Immutable. + */ +@JsonSerialize(using = Rate.RateSerializer.class) +@JsonDeserialize(using = Rate.RateDeserializer.class) +public class Rate { + public static final String MESSAGE_CONSTRAINTS = "The rate should not be negative or " + + "have more than 2 decimal places."; + public static final String MESSAGE_TOO_LARGE = "The value for the rate is too large. " + + "Are you sure you need to pay this employee more than $1000000 per hour?"; + + public final Money amount; + public final Duration duration; + + /** + * Constructs a {@code Rate} instance. + * + * @param amount Money per unit time. + * @param duration Unit time duration. + * @throws NegativeMoneyValueException Throws when money is negative. + */ + public Rate(Money amount, Duration duration) throws NegativeMoneyValueException { + requireAllNonNull(amount, duration); + requireNonNegative(amount); + requirePositive(duration); + this.amount = amount; + this.duration = duration; + } + + public Money getAmount() { + return amount; + } + + public Duration getDuration() { + return duration; + } + + /** + * Calculates the resulting amount of {@code Money} from multiplying this rate + * by the given + * {@code Duration}. + * + * @param totalDuration the duration to multiply this rate by + * @return the resulting amount of {@code Money} at this rate for the given + * duration + */ + public Money calculateAmount(Duration totalDuration) { + return amount.multiply(BigDecimal.valueOf(totalDuration.dividedBy(duration))); + + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Rate)) { + return false; + } + + Rate otherRate = (Rate) other; + + return amount.equals(otherRate.amount) + && duration.equals(otherRate.duration); + } + + @Override + public int hashCode() { + return Objects.hash(amount, duration); + } + + /** + * Prints the 2 decimal place currency format of the value. + * + * @return Value in currency format as a string. + */ + @Override + public String toString() { + BigDecimal value = amount.getValue(); + float hours = duration.toHours(); + BigDecimal perHour = value.divide(BigDecimal.valueOf(hours), RoundingMode.HALF_UP); + + // @@author Sergey Vyacheslavovich Brunov for converting big decimal to 2dp + // retrieved from https://stackoverflow.com/a/10457320/16777554 + DecimalFormat df = new DecimalFormat(); + df.setMaximumFractionDigits(2); + df.setMinimumFractionDigits(0); + df.setGroupingUsed(false); + + String result = df.format(perHour); + + return String.format("$%s/h", result); + } + + protected static class RateSerializer extends StdSerializer { + private RateSerializer(Class val) { + super(val); + } + + private RateSerializer() { + this(null); + } + + @Override + public void serialize(Rate val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartObject(); + + gen.writeObjectField("amount", val.getAmount()); + gen.writeStringField("duration", val.getDuration().toString()); + + gen.writeEndObject(); + } + } + + protected static class RateDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The rate instance is invalid or missing!"; + private static final UnaryOperator INVALID_VAL_FMTR = + k -> String.format("This rate's %s value is invalid!", k); + + private RateDeserializer(Class vc) { + super(vc); + } + + private RateDeserializer() { + this(null); + } + + private static JsonNode getNonNullNode(ObjectNode node, String key, DeserializationContext ctx) + throws JsonMappingException { + return JsonUtil.getNonNullNode(node, key, ctx, INVALID_VAL_FMTR); + } + + private static T getNonNullNodeWithType(ObjectNode node, String key, DeserializationContext ctx, + Class cls) throws JsonMappingException { + return JsonUtil.getNonNullNodeWithType(node, key, ctx, + INVALID_VAL_FMTR, cls); + } + + @Override + public Rate deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + ObjectCodec codec = p.getCodec(); + + if (!(node instanceof ObjectNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + ObjectNode rate = (ObjectNode) node; + + Money amount = getNonNullNode(rate, "amount", ctx) + .traverse(codec) + .readValueAs(Money.class); + + String durationString = getNonNullNodeWithType(rate, "duration", ctx, TextNode.class) + .textValue(); + + Duration duration; + try { + duration = Duration.parse(durationString); + } catch (NullPointerException | DateTimeParseException e) { + throw JsonUtil.getWrappedIllegalValueException( + ctx, INVALID_VAL_FMTR.apply("duration"), e); + } + + return new Rate(amount, duration); + } + + @Override + public Rate getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/money/exceptions/NegativeMoneyValueException.java b/src/main/java/peoplesoft/model/money/exceptions/NegativeMoneyValueException.java new file mode 100644 index 00000000000..18848a04b29 --- /dev/null +++ b/src/main/java/peoplesoft/model/money/exceptions/NegativeMoneyValueException.java @@ -0,0 +1,10 @@ +package peoplesoft.model.money.exceptions; + +/** + * Signals that the operation will result in money with negative value. + */ +public class NegativeMoneyValueException extends RuntimeException { + public NegativeMoneyValueException() { + super("Operation would result in money with negative value"); + } +} diff --git a/src/main/java/peoplesoft/model/money/exceptions/PaymentAlreadyPaidException.java b/src/main/java/peoplesoft/model/money/exceptions/PaymentAlreadyPaidException.java new file mode 100644 index 00000000000..9f76fc30b56 --- /dev/null +++ b/src/main/java/peoplesoft/model/money/exceptions/PaymentAlreadyPaidException.java @@ -0,0 +1,6 @@ +package peoplesoft.model.money.exceptions; + +/** + * Signals that the Payment has already been paid for. + */ +public class PaymentAlreadyPaidException extends RuntimeException {} diff --git a/src/main/java/peoplesoft/model/money/exceptions/PaymentRequiresPersonException.java b/src/main/java/peoplesoft/model/money/exceptions/PaymentRequiresPersonException.java new file mode 100644 index 00000000000..013b22814eb --- /dev/null +++ b/src/main/java/peoplesoft/model/money/exceptions/PaymentRequiresPersonException.java @@ -0,0 +1,3 @@ +package peoplesoft.model.money.exceptions; + +public class PaymentRequiresPersonException extends RuntimeException {} diff --git a/src/main/java/peoplesoft/model/person/Address.java b/src/main/java/peoplesoft/model/person/Address.java new file mode 100644 index 00000000000..3b60b85a772 --- /dev/null +++ b/src/main/java/peoplesoft/model/person/Address.java @@ -0,0 +1,123 @@ +package peoplesoft.model.person; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.AppUtil.checkArgument; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; + +/** + * Represents a Person's address in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} + */ +@JsonSerialize(using = Address.AddressSerializer.class) +@JsonDeserialize(using = Address.AddressDeserializer.class) +public class Address { + public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "[^\\s].*"; + + public final String value; + + /** + * Constructs an {@code Address}. + * + * @param address A valid address. + */ + public Address(String address) { + requireNonNull(address); + checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); + value = address; + } + + /** + * Returns true if a given string is a valid email. + */ + public static boolean isValidAddress(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Address // instanceof handles nulls + && value.equals(((Address) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + protected static class AddressSerializer extends StdSerializer
{ + private AddressSerializer(Class
val) { + super(val); + } + + private AddressSerializer() { + this(null); + } + + @Override + public void serialize(Address val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(val.value); + } + } + + protected static class AddressDeserializer extends StdDeserializer
{ + private static final String MISSING_OR_INVALID_INSTANCE = "The address value is invalid or missing!"; + + private AddressDeserializer(Class vc) { + super(vc); + } + + private AddressDeserializer() { + this(null); + } + + @Override + public Address deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + + if (!(node instanceof TextNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + String address = ((TextNode) node).textValue(); + if (!Address.isValidAddress(address)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, Address.MESSAGE_CONSTRAINTS); + } + + return new Address(address); + } + + @Override + public Address getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/person/Email.java b/src/main/java/peoplesoft/model/person/Email.java new file mode 100644 index 00000000000..8632be3b423 --- /dev/null +++ b/src/main/java/peoplesoft/model/person/Email.java @@ -0,0 +1,147 @@ +package peoplesoft.model.person; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.AppUtil.checkArgument; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; + +/** + * Represents a Person's email in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} + */ +@JsonSerialize(using = Email.EmailSerializer.class) +@JsonDeserialize(using = Email.EmailDeserializer.class) +public class Email { + + private static final String SPECIAL_CHARACTERS = "+_.-"; + private static final String SPECIAL_CHARACTERS_NO_PERIOD = "+_-"; + public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format name@domain " + + "and adhere to the following constraints:\n" + + "1. The name should only contain alphanumeric characters and these special characters, excluding " + + "the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " + + "characters. Scroll down to read more.\n" + + "2. This is followed by a '@' and then a domain name. The domain name is made up of domain labels " + + "separated by periods.\n" + + "The domain name must:\n" + + " - end with a domain label at least 2 characters long\n" + + " - have each domain label start and end with alphanumeric characters\n" + + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any."; + // alphanumeric and special characters + private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]"; // alphanumeric characters except underscore + + private static final String LOCAL_PART_REGEX = "^" + + ALPHANUMERIC_NO_UNDERSCORE + "+" + + "(" + + "([" + SPECIAL_CHARACTERS_NO_PERIOD + "]*|\\.)" + + ALPHANUMERIC_NO_UNDERSCORE + "+" + + ")*"; + private static final String DOMAIN_PART_REGEX = ALPHANUMERIC_NO_UNDERSCORE + "+" + + "(" + + "-*" + + ALPHANUMERIC_NO_UNDERSCORE + "+" + + ")*"; + private static final String DOMAIN_LAST_PART_REGEX = "(" + DOMAIN_PART_REGEX + "){2,}$"; // At least two chars + private static final String DOMAIN_REGEX = "(" + DOMAIN_PART_REGEX + "\\.)*" + DOMAIN_LAST_PART_REGEX; + public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" + DOMAIN_REGEX; + + public final String value; + + /** + * Constructs an {@code Email}. + * + * @param email A valid email address. + */ + public Email(String email) { + requireNonNull(email); + checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); + value = email; + } + + /** + * Returns if a given string is a valid email. + */ + public static boolean isValidEmail(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Email // instanceof handles nulls + && value.equals(((Email) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + protected static class EmailSerializer extends StdSerializer { + private EmailSerializer(Class val) { + super(val); + } + + private EmailSerializer() { + this(null); + } + + @Override + public void serialize(Email val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(val.value); + } + } + + protected static class EmailDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The email value is invalid or missing!"; + + private EmailDeserializer(Class vc) { + super(vc); + } + + private EmailDeserializer() { + this(null); + } + + @Override + public Email deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + + if (!(node instanceof TextNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + String email = ((TextNode) node).textValue(); + if (!Email.isValidEmail(email)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, Email.MESSAGE_CONSTRAINTS); + } + + return new Email(email); + } + + @Override + public Email getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/person/Name.java b/src/main/java/peoplesoft/model/person/Name.java new file mode 100644 index 00000000000..83a404b6cd1 --- /dev/null +++ b/src/main/java/peoplesoft/model/person/Name.java @@ -0,0 +1,125 @@ +package peoplesoft.model.person; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.AppUtil.checkArgument; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; + +/** + * Represents a Person's name in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} + */ +@JsonSerialize(using = Name.NameSerializer.class) +@JsonDeserialize(using = Name.NameDeserializer.class) +public class Name { + public static final String MESSAGE_CONSTRAINTS = + "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + + public final String fullName; + + /** + * Constructs a {@code Name}. + * + * @param name A valid name. + */ + public Name(String name) { + requireNonNull(name); + checkArgument(isValidName(name), MESSAGE_CONSTRAINTS); + fullName = name; + } + + /** + * Returns true if a given string is a valid name. + */ + public static boolean isValidName(String test) { + return test.matches(VALIDATION_REGEX); + } + + + @Override + public String toString() { + return fullName; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Name // instanceof handles nulls + && fullName.equals(((Name) other).fullName)); // state check + } + + @Override + public int hashCode() { + return fullName.hashCode(); + } + + protected static class NameSerializer extends StdSerializer { + private NameSerializer(Class val) { + super(val); + } + + private NameSerializer() { + this(null); + } + + @Override + public void serialize(Name val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(val.fullName); + } + } + + protected static class NameDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The name value is invalid or missing!"; + + private NameDeserializer(Class vc) { + super(vc); + } + + private NameDeserializer() { + this(null); + } + + @Override + public Name deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + + if (!(node instanceof TextNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + String name = ((TextNode) node).textValue(); + if (!Name.isValidName(name)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, Name.MESSAGE_CONSTRAINTS); + } + + return new Name(name); + } + + @Override + public Name getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/person/Person.java b/src/main/java/peoplesoft/model/person/Person.java new file mode 100644 index 00000000000..7ea48d4247f --- /dev/null +++ b/src/main/java/peoplesoft/model/person/Person.java @@ -0,0 +1,334 @@ +package peoplesoft.model.person; + +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; +import peoplesoft.model.money.Money; +import peoplesoft.model.money.Payment; +import peoplesoft.model.money.Rate; +import peoplesoft.model.tag.Tag; +import peoplesoft.model.util.ID; + +/** + * Represents a Person in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +@JsonSerialize(using = Person.PersonSerializer.class) +@JsonDeserialize(using = Person.PersonDeserializer.class) +public class Person { + private final ID id; + + // Identity fields + private final Name name; + private final Phone phone; + private final Email email; + + // Data fields + private final Address address; + private final Rate rate; + private final Set tags = new HashSet<>(); + private final Map payments = new HashMap<>(); + + /** + * Every field must be present and not null. + */ + public Person(ID id, Name name, Phone phone, Email email, Address address, Rate rate, Set tags, + Map payments) { + requireAllNonNull(id, name, phone, email, address, rate, tags); + this.id = id; + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.rate = rate; + this.tags.addAll(tags); + this.payments.putAll(payments); + } + + public ID getPersonId() { + return id; + } + + public Name getName() { + return name; + } + + public Phone getPhone() { + return phone; + } + + public Email getEmail() { + return email; + } + + public Address getAddress() { + return address; + } + + public Rate getRate() { + return rate; + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + public Map getPayments() { + return Collections.unmodifiableMap(payments); + } + + /** + * Calculates the Money of all the {@code PendingPayment}s on this {@code Person} at a point in time + * + * @return Amount due. + */ + public Money getAmountDue() { + Money sum = new Money(0); + for (Payment p : payments.values()) { + if (!p.isCompleted()) { + sum = sum.add(p.getAmount()); + } + } + return sum; + } + + /** + * 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 + && getPersonId().equals(otherPerson.getPersonId()); + } + + /** + * Returns true if both persons have the data fields. + * This defines a weak notion of equality between two persons. + */ + public boolean isDuplicate(Person otherPerson) { + if (otherPerson == this) { + return true; + } + + return otherPerson.getName().equals(getName()) + && otherPerson.getPhone().equals(getPhone()) + && otherPerson.getEmail().equals(getEmail()) + && otherPerson.getAddress().equals(getAddress()) + && otherPerson.getRate().equals(getRate()) + && otherPerson.getTags().equals(getTags()) + && otherPerson.getPayments().equals(getPayments()); + } + + /** + * 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 isSamePerson(otherPerson) + && otherPerson.getName().equals(getName()) + && otherPerson.getPhone().equals(getPhone()) + && otherPerson.getEmail().equals(getEmail()) + && otherPerson.getAddress().equals(getAddress()) + && otherPerson.getRate().equals(getRate()) + && otherPerson.getTags().equals(getTags()) + && otherPerson.getPayments().equals(getPayments()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(id, name, phone, email, address, rate, tags, payments); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("ID: ") + .append(getPersonId()) + .append("; Name: ") + .append(getName()) + .append("; Phone: ") + .append(getPhone()) + .append("; Email: ") + .append(getEmail()) + .append("; Address: ") + .append(getAddress()) + .append("; Base Pay Rate: ") + .append(getRate()); + + Set tags = getTags(); + if (!tags.isEmpty()) { + builder.append("; Tags: "); + tags.forEach(builder::append); + } + + Map payments = getPayments(); + if (!payments.isEmpty()) { + builder.append("; Payments: ["); + builder.append(payments.values().stream().map(String::valueOf).collect(Collectors.joining(", "))); + builder.append("]"); + } + return builder.toString(); + } + + protected static class PersonSerializer extends StdSerializer { + private PersonSerializer(Class val) { + super(val); + } + + private PersonSerializer() { + this(null); + } + + @Override + public void serialize(Person val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartObject(); + + gen.writeObjectField("id", val.getPersonId()); + gen.writeObjectField("name", val.getName()); + gen.writeObjectField("phone", val.getPhone()); + gen.writeObjectField("email", val.getEmail()); + gen.writeObjectField("address", val.getAddress()); + gen.writeObjectField("rate", val.getRate()); + gen.writeObjectField("tagged", val.getTags()); + + gen.writeArrayFieldStart("payments"); + for (Payment pymt : val.getPayments().values()) { + gen.writeObject(pymt); + } + gen.writeEndArray(); + + gen.writeEndObject(); + } + } + + protected static class PersonDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The person instance is invalid or missing!"; + private static final UnaryOperator INVALID_VAL_FMTR = + k -> String.format("This person's %s value is invalid!", k); + + private PersonDeserializer(Class vc) { + super(vc); + } + + private PersonDeserializer() { + this(null); + } + + private static JsonNode getNonNullNode(ObjectNode node, String key, DeserializationContext ctx) + throws JsonMappingException { + return JsonUtil.getNonNullNode(node, key, ctx, INVALID_VAL_FMTR); + } + + private static T getNonNullNodeWithType(ObjectNode node, String key, DeserializationContext ctx, + Class cls) throws JsonMappingException { + return JsonUtil.getNonNullNodeWithType(node, key, ctx, + INVALID_VAL_FMTR, cls); + } + + @Override + public Person deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + ObjectCodec codec = p.getCodec(); + + if (!(node instanceof ObjectNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + ObjectNode person = (ObjectNode) node; + + ID id = getNonNullNode(person, "id", ctx) + .traverse(codec) + .readValueAs(ID.class); + + Name name = getNonNullNode(person, "name", ctx) + .traverse(codec) + .readValueAs(Name.class); + + Phone phone = getNonNullNode(person, "phone", ctx) + .traverse(codec) + .readValueAs(Phone.class); + + Email email = getNonNullNode(person, "email", ctx) + .traverse(codec) + .readValueAs(Email.class); + + Address address = getNonNullNode(person, "address", ctx) + .traverse(codec) + .readValueAs(Address.class); + + Rate rate = getNonNullNode(person, "rate", ctx) + .traverse(codec) + .readValueAs(Rate.class); + + Set tags = getNonNullNodeWithType(person, "tagged", ctx, ArrayNode.class) + .traverse(codec) + .readValueAs(new TypeReference>(){}); + + // we deserialize the Payment objects one by one + // because ctx.readValue() doesn't take TypeReferences + // and we need to use the ctx to pass the current person ID down + ctx.setAttribute("personId", id); + Map payments = new HashMap<>(); + ArrayNode paymentsNode = getNonNullNodeWithType(person, "payments", ctx, ArrayNode.class); + for (JsonNode paymentNode : paymentsNode) { + Payment pymt = ctx.readValue(paymentNode.traverse(codec), Payment.class); + if (payments.put(pymt.getJobId(), pymt) != null) { // check if jobId already exists in the map + throw JsonUtil.getWrappedIllegalValueException(ctx, INVALID_VAL_FMTR.apply("payments")); + } + } + + return new Person(id, name, phone, email, address, rate, tags, payments); + } + + @Override + public Person getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/person/PersonContainsKeywordsPredicate.java b/src/main/java/peoplesoft/model/person/PersonContainsKeywordsPredicate.java new file mode 100644 index 00000000000..68c0783a592 --- /dev/null +++ b/src/main/java/peoplesoft/model/person/PersonContainsKeywordsPredicate.java @@ -0,0 +1,34 @@ +package peoplesoft.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import peoplesoft.commons.util.StringUtil; + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class PersonContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public PersonContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return !keywords.isEmpty() && keywords.stream() + .allMatch(keyword -> ( + person.getTags().stream().anyMatch( + tag -> keyword.equalsIgnoreCase(tag.getTagName()))) + || StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PersonContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((PersonContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/peoplesoft/model/person/Phone.java b/src/main/java/peoplesoft/model/person/Phone.java new file mode 100644 index 00000000000..dd6ff0aa3e6 --- /dev/null +++ b/src/main/java/peoplesoft/model/person/Phone.java @@ -0,0 +1,118 @@ +package peoplesoft.model.person; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.AppUtil.checkArgument; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; + +/** + * Represents a Person's phone number in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} + */ +@JsonSerialize(using = Phone.PhoneSerializer.class) +@JsonDeserialize(using = Phone.PhoneDeserializer.class) +public class Phone { + public static final String MESSAGE_CONSTRAINTS = + "Phone numbers should only contain numbers, and it should be at least 3 digits long"; + public static final String VALIDATION_REGEX = "\\d{3,}"; + public final String value; + + /** + * Constructs a {@code Phone}. + * + * @param phone A valid phone number. + */ + public Phone(String phone) { + requireNonNull(phone); + checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); + value = phone; + } + + /** + * Returns true if a given string is a valid phone number. + */ + public static boolean isValidPhone(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 Phone // instanceof handles nulls + && value.equals(((Phone) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + protected static class PhoneSerializer extends StdSerializer { + private PhoneSerializer(Class val) { + super(val); + } + + private PhoneSerializer() { + this(null); + } + + @Override + public void serialize(Phone val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(val.value); + } + } + + protected static class PhoneDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The phone value is invalid or missing!"; + + private PhoneDeserializer(Class vc) { + super(vc); + } + + private PhoneDeserializer() { + this(null); + } + + @Override + public Phone deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + + if (!(node instanceof TextNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + String phone = ((TextNode) node).textValue(); + if (!Phone.isValidPhone(phone)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, Phone.MESSAGE_CONSTRAINTS); + } + + return new Phone(phone); + } + + @Override + public Phone getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/person/UniquePersonList.java b/src/main/java/peoplesoft/model/person/UniquePersonList.java new file mode 100644 index 00000000000..8808400f50f --- /dev/null +++ b/src/main/java/peoplesoft/model/person/UniquePersonList.java @@ -0,0 +1,230 @@ +package peoplesoft.model.person; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.CollectionUtil.requireAllNonNull; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import peoplesoft.commons.util.JsonUtil; +import peoplesoft.model.person.exceptions.DuplicatePersonException; +import peoplesoft.model.person.exceptions.PersonNotFoundException; +import peoplesoft.model.util.ID; + +/** + * 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) + */ +@JsonSerialize(using = UniquePersonList.UniquePersonListSerializer.class) +@JsonDeserialize(using = UniquePersonList.UniquePersonListDeserializer.class) +public class UniquePersonList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains a person with the same ID as the given argument. + */ + public boolean contains(ID personId) { + requireNonNull(personId); + return internalList.stream().anyMatch(p -> p != null && personId.equals(p.getPersonId())); + } + + /** + * Returns true if the list contains a person with the same data fields as the given argument. + */ + public boolean contains(Person person) { + requireNonNull(person); + return internalList.stream().anyMatch(person::isDuplicate); + } + + /** + * Returns the job with the given id. + * + * @throws PersonNotFoundException if the person does not exist + */ + public Person get(ID personId) throws PersonNotFoundException { + requireNonNull(personId); + return internalList.stream() + .filter(p -> p != null && personId.equals(p.getPersonId())) + .findAny() + .orElseThrow(PersonNotFoundException::new); + } + + /** + * 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; + } + + protected static class UniquePersonListSerializer extends StdSerializer { + private UniquePersonListSerializer(Class val) { + super(val); + } + + private UniquePersonListSerializer() { + this(null); + } + + @Override + public void serialize(UniquePersonList val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeObject(val.asUnmodifiableObservableList()); + } + } + + protected static class UniquePersonListDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The person list is invalid or missing!"; + + private UniquePersonListDeserializer(Class vc) { + super(vc); + } + + private UniquePersonListDeserializer() { + this(null); + } + + @Override + public UniquePersonList deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + ObjectCodec codec = p.getCodec(); + + if (!(node instanceof ArrayNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + List personList = node // is ArrayNode + .traverse(codec) + .readValueAs(new TypeReference>(){}); + + UniquePersonList upl = new UniquePersonList(); + upl.setPersons(personList); + + return upl; + } + + @Override + public UniquePersonList getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/peoplesoft/model/person/exceptions/DuplicatePersonException.java similarity index 87% rename from src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java rename to src/main/java/peoplesoft/model/person/exceptions/DuplicatePersonException.java index d7290f59442..3d44dbd5f30 100644 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ b/src/main/java/peoplesoft/model/person/exceptions/DuplicatePersonException.java @@ -1,4 +1,4 @@ -package seedu.address.model.person.exceptions; +package peoplesoft.model.person.exceptions; /** * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/peoplesoft/model/person/exceptions/PersonNotFoundException.java similarity index 75% rename from src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java rename to src/main/java/peoplesoft/model/person/exceptions/PersonNotFoundException.java index fa764426ca7..2af0baa26b2 100644 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ b/src/main/java/peoplesoft/model/person/exceptions/PersonNotFoundException.java @@ -1,4 +1,4 @@ -package seedu.address.model.person.exceptions; +package peoplesoft.model.person.exceptions; /** * Signals that the operation is unable to find the specified person. diff --git a/src/main/java/peoplesoft/model/tag/Tag.java b/src/main/java/peoplesoft/model/tag/Tag.java new file mode 100644 index 00000000000..6ddb5369afe --- /dev/null +++ b/src/main/java/peoplesoft/model/tag/Tag.java @@ -0,0 +1,125 @@ +package peoplesoft.model.tag; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.AppUtil.checkArgument; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; + +/** + * Represents a Tag in the address book. + * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} + */ +@JsonSerialize(using = Tag.TagSerializer.class) +@JsonDeserialize(using = Tag.TagDeserializer.class) +public class Tag { + + public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; + public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + + public final String tagName; + + /** + * Constructs a {@code Tag}. + * + * @param tagName A valid tag name. + */ + public Tag(String tagName) { + requireNonNull(tagName); + checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); + this.tagName = tagName; + } + + /** + * Returns true if a given string is a valid tag name. + */ + public static boolean isValidTagName(String test) { + return test.matches(VALIDATION_REGEX); + } + + public String getTagName() { + return tagName; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Tag // instanceof handles nulls + && tagName.equals(((Tag) other).tagName)); // state check + } + + @Override + public int hashCode() { + return tagName.hashCode(); + } + + /** + * Format state as text for viewing. + */ + public String toString() { + return '[' + tagName + ']'; + } + + protected static class TagSerializer extends StdSerializer { + private TagSerializer(Class val) { + super(val); + } + + private TagSerializer() { + this(null); + } + + @Override + public void serialize(Tag val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(val.tagName); + } + } + + protected static class TagDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The tag value is invalid or missing!"; + + private TagDeserializer(Class vc) { + super(vc); + } + + private TagDeserializer() { + this(null); + } + + @Override + public Tag deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + + if (!(node instanceof TextNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + String tag = ((TextNode) node).textValue(); + if (!Tag.isValidTagName(tag)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, Tag.MESSAGE_CONSTRAINTS); + } + + return new Tag(tag); + } + + @Override + public Tag getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/util/ID.java b/src/main/java/peoplesoft/model/util/ID.java new file mode 100644 index 00000000000..23f5f9b5449 --- /dev/null +++ b/src/main/java/peoplesoft/model/util/ID.java @@ -0,0 +1,142 @@ +package peoplesoft.model.util; + +import static java.util.Objects.requireNonNull; +import static peoplesoft.commons.util.AppUtil.checkArgument; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import peoplesoft.commons.util.JsonUtil; + +/** + * Represents an ID for some sort of entity, e.g. a {@code Person} or {@code Job}. + * Guarantees: immutable; is valid as declared in {@link #isValidId(String)} + */ +@JsonSerialize(using = ID.IdSerializer.class) +@JsonDeserialize(using = ID.IdDeserializer.class) +public class ID implements Comparable { + public static final String MESSAGE_CONSTRAINTS = + "IDs should only begin and end with alphanumeric characters, " + + "contain alphanumeric characters and hyphens, " + + "and should not be blank"; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "[\\p{Alnum}]([\\p{Alnum}-]*[\\p{Alnum}])?"; + + public final String value; + + /** + * Constructs a {@code ID}. + * + * @param value A valid id. + */ + public ID(String value) { + requireNonNull(value); + checkArgument(isValidId(value), MESSAGE_CONSTRAINTS); + this.value = value; + } + + /** + * Constructs a {@code ID}. + * + * @param value A valid id. + */ + public ID(int value) { + String strValue = String.valueOf(value); + checkArgument(isValidId(strValue), MESSAGE_CONSTRAINTS); + this.value = strValue; + } + + /** + * Returns true if a given string is a valid id. + */ + public static boolean isValidId(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 ID // instanceof handles nulls + && value.equals(((ID) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public int compareTo(ID o) { + return value.compareTo(o.value); + } + + protected static class IdSerializer extends StdSerializer { + private IdSerializer(Class val) { + super(val); + } + + private IdSerializer() { + this(null); + } + + @Override + public void serialize(ID val, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(val.value); + } + } + + protected static class IdDeserializer extends StdDeserializer { + private static final String MISSING_OR_INVALID_INSTANCE = "The id value is invalid or missing!"; + + private IdDeserializer(Class vc) { + super(vc); + } + + private IdDeserializer() { + this(null); + } + + @Override + public ID deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + JsonNode node = p.readValueAsTree(); + + if (!(node instanceof TextNode)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + + String value = ((TextNode) node).textValue(); + if (!ID.isValidId(value)) { + throw JsonUtil.getWrappedIllegalValueException(ctx, ID.MESSAGE_CONSTRAINTS); + } + + return new ID(value); + } + + @Override + public ID getNullValue(DeserializationContext ctx) throws JsonMappingException { + throw JsonUtil.getWrappedIllegalValueException(ctx, MISSING_OR_INVALID_INSTANCE); + } + } +} diff --git a/src/main/java/peoplesoft/model/util/SampleDataUtil.java b/src/main/java/peoplesoft/model/util/SampleDataUtil.java new file mode 100644 index 00000000000..fc2059d2d10 --- /dev/null +++ b/src/main/java/peoplesoft/model/util/SampleDataUtil.java @@ -0,0 +1,67 @@ +package peoplesoft.model.util; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import peoplesoft.commons.core.PersonIdFactory; +import peoplesoft.model.AddressBook; +import peoplesoft.model.ReadOnlyAddressBook; +import peoplesoft.model.money.Money; +import peoplesoft.model.money.Rate; +import peoplesoft.model.person.Address; +import peoplesoft.model.person.Email; +import peoplesoft.model.person.Name; +import peoplesoft.model.person.Person; +import peoplesoft.model.person.Phone; +import peoplesoft.model.tag.Tag; + +/** + * Contains utility methods for populating {@code AddressBook} with sample data. + */ +public class SampleDataUtil { + public static Person[] getSamplePersons() { + return new Person[] { + new Person(PersonIdFactory.nextId(), new Name("Nicole Tan"), new Phone("99338558"), + new Email("nicole@stffhub.org"), new Address("1 Tech Drive, S138572"), + new Rate(new Money(30), Duration.ofHours(1)), getTagSet("Intern", "Aircon"), + Map.of()), + new Person(PersonIdFactory.nextId(), new Name("Kavya Singh"), new Phone("96736637"), + new Email("kavya@stffhub.org"), new Address("2 Orchard Turn, S238801"), + new Rate(new Money(40), Duration.ofHours(1)), getTagSet("Senior", "Electrician"), + Map.of()), + new Person(PersonIdFactory.nextId(), new Name("Ethan Lee"), new Phone("91031282"), + new Email("ethan@stffhub.org"), new Address("10 Anson Road, S079903"), + new Rate(new Money(20), Duration.ofHours(1)), getTagSet("Appliances"), + Map.of()), + new Person(PersonIdFactory.nextId(), new Name("Irfan Ibrahim"), new Phone("92492021"), + new Email("irfan@stffhub.org"), new Address("Blk 47 Tampines Street 20, #17-35"), + new Rate(new Money(48), Duration.ofHours(1)), getTagSet("Painting"), + Map.of()), + new Person(PersonIdFactory.nextId(), new Name("Arjun Khatau"), new Phone("80445044"), + new Email("arjun@stffhub.org"), new Address("50 Collyer Quay, S049321"), + new Rate(new Money(33), Duration.ofHours(1)), getTagSet("Contract", "Aircon"), + Map.of()) + }; + } + + public static ReadOnlyAddressBook getSampleAddressBook() { + AddressBook sampleAb = new AddressBook(); + for (Person samplePerson : getSamplePersons()) { + sampleAb.addPerson(samplePerson); + } + return sampleAb; + } + + /** + * Returns a tag set containing the list of strings given. + */ + public static Set getTagSet(String... strings) { + return Arrays.stream(strings) + .map(Tag::new) + .collect(Collectors.toSet()); + } + +} diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/peoplesoft/storage/AddressBookStorage.java similarity index 85% rename from src/main/java/seedu/address/storage/AddressBookStorage.java rename to src/main/java/peoplesoft/storage/AddressBookStorage.java index 4599182b3f9..602f138ea6d 100644 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ b/src/main/java/peoplesoft/storage/AddressBookStorage.java @@ -1,14 +1,14 @@ -package seedu.address.storage; +package peoplesoft.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; +import peoplesoft.commons.exceptions.DataConversionException; +import peoplesoft.model.ReadOnlyAddressBook; /** - * Represents a storage for {@link seedu.address.model.AddressBook}. + * Represents a storage for {@link peoplesoft.model.AddressBook}. */ public interface AddressBookStorage { diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/peoplesoft/storage/JsonAddressBookStorage.java similarity index 67% rename from src/main/java/seedu/address/storage/JsonAddressBookStorage.java rename to src/main/java/peoplesoft/storage/JsonAddressBookStorage.java index dfab9daaa0d..f62eace7b80 100644 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ b/src/main/java/peoplesoft/storage/JsonAddressBookStorage.java @@ -1,4 +1,4 @@ -package seedu.address.storage; +package peoplesoft.storage; import static java.util.Objects.requireNonNull; @@ -7,12 +7,12 @@ import java.util.Optional; import java.util.logging.Logger; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.commons.util.FileUtil; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyAddressBook; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.commons.exceptions.DataConversionException; +import peoplesoft.commons.util.FileUtil; +import peoplesoft.commons.util.JsonUtil; +import peoplesoft.model.AddressBook; +import peoplesoft.model.ReadOnlyAddressBook; /** * A class to access AddressBook data stored as a json file on the hard disk. @@ -45,18 +45,14 @@ public Optional readAddressBook() throws DataConversionExce public Optional readAddressBook(Path filePath) throws DataConversionException { requireNonNull(filePath); - Optional jsonAddressBook = JsonUtil.readJsonFile( - filePath, JsonSerializableAddressBook.class); + Optional jsonAddressBook = JsonUtil.readJsonFile( + filePath, AddressBook.class); + if (!jsonAddressBook.isPresent()) { return Optional.empty(); } - try { - return Optional.of(jsonAddressBook.get().toModelType()); - } catch (IllegalValueException ive) { - logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); - throw new DataConversionException(ive); - } + return jsonAddressBook; } @Override @@ -74,7 +70,7 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro requireNonNull(filePath); FileUtil.createIfMissing(filePath); - JsonUtil.saveJsonFile(new JsonSerializableAddressBook(addressBook), filePath); + JsonUtil.saveJsonFile(addressBook, filePath); } } diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/peoplesoft/storage/JsonUserPrefsStorage.java similarity index 83% rename from src/main/java/seedu/address/storage/JsonUserPrefsStorage.java rename to src/main/java/peoplesoft/storage/JsonUserPrefsStorage.java index bc2bbad84aa..eeb3c71ee01 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/peoplesoft/storage/JsonUserPrefsStorage.java @@ -1,13 +1,13 @@ -package seedu.address.storage; +package peoplesoft.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import peoplesoft.commons.exceptions.DataConversionException; +import peoplesoft.commons.util.JsonUtil; +import peoplesoft.model.ReadOnlyUserPrefs; +import peoplesoft.model.UserPrefs; /** * A class to access UserPrefs stored in the hard disk as a json file diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/peoplesoft/storage/Storage.java similarity index 73% rename from src/main/java/seedu/address/storage/Storage.java rename to src/main/java/peoplesoft/storage/Storage.java index beda8bd9f11..e5b7f17a57f 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/peoplesoft/storage/Storage.java @@ -1,13 +1,13 @@ -package seedu.address.storage; +package peoplesoft.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; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import peoplesoft.commons.exceptions.DataConversionException; +import peoplesoft.model.ReadOnlyAddressBook; +import peoplesoft.model.ReadOnlyUserPrefs; +import peoplesoft.model.UserPrefs; /** * API of the Storage component diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/peoplesoft/storage/StorageManager.java similarity index 89% rename from src/main/java/seedu/address/storage/StorageManager.java rename to src/main/java/peoplesoft/storage/StorageManager.java index 6cfa0162164..5e27bf74f58 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/peoplesoft/storage/StorageManager.java @@ -1,15 +1,15 @@ -package seedu.address.storage; +package peoplesoft.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; import java.util.logging.Logger; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.commons.exceptions.DataConversionException; +import peoplesoft.model.ReadOnlyAddressBook; +import peoplesoft.model.ReadOnlyUserPrefs; +import peoplesoft.model.UserPrefs; /** * Manages storage of AddressBook data in local storage. diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/peoplesoft/storage/UserPrefsStorage.java similarity index 71% rename from src/main/java/seedu/address/storage/UserPrefsStorage.java rename to src/main/java/peoplesoft/storage/UserPrefsStorage.java index 29eef178dbc..23a4e87f192 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/peoplesoft/storage/UserPrefsStorage.java @@ -1,15 +1,15 @@ -package seedu.address.storage; +package peoplesoft.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import peoplesoft.commons.exceptions.DataConversionException; +import peoplesoft.model.ReadOnlyUserPrefs; +import peoplesoft.model.UserPrefs; /** - * Represents a storage for {@link seedu.address.model.UserPrefs}. + * Represents a storage for {@link peoplesoft.model.UserPrefs}. */ public interface UserPrefsStorage { @@ -27,7 +27,7 @@ public interface UserPrefsStorage { Optional readUserPrefs() throws DataConversionException, IOException; /** - * Saves the given {@link seedu.address.model.ReadOnlyUserPrefs} to the storage. + * Saves the given {@link peoplesoft.model.ReadOnlyUserPrefs} to the storage. * @param userPrefs cannot be null. * @throws IOException if there was any problem writing to the file. */ diff --git a/src/main/java/peoplesoft/ui/MainWindow.java b/src/main/java/peoplesoft/ui/MainWindow.java new file mode 100644 index 00000000000..fb1919ea77d --- /dev/null +++ b/src/main/java/peoplesoft/ui/MainWindow.java @@ -0,0 +1,199 @@ +package peoplesoft.ui; + +import java.util.Objects; +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.text.Font; +import javafx.stage.Stage; +import peoplesoft.commons.core.GuiSettings; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.logic.Logic; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.model.Model; +import peoplesoft.ui.regions.CommandBox; +import peoplesoft.ui.regions.ResultDisplay; +import peoplesoft.ui.regions.SideBar; +import peoplesoft.ui.scenes.HelpPage; +import peoplesoft.ui.scenes.OverviewPage; +import peoplesoft.ui.scenes.Page; + +/** + * The Main Window. Provides the basic application layout containing + * a menu bar and space where other JavaFX elements can be placed. + */ +public class MainWindow extends UiPart { + + private static final String FXML = "MainWindow.fxml"; + private static final int UNUSED_FONT_SIZE = 10; + + private final Logger logger = LogsCenter.getLogger(getClass()); + + private Stage primaryStage; + private Logic logic; + private Model model; + + // Independent Ui parts residing in this Ui container + private SideBar sideBar; + private ResultDisplay resultDisplay; + private OverviewPage overviewPage; + private HelpPage helpPage; + private PageSwitcher pageSwitcher; + + @FXML + private BorderPane bp; + + @FXML + private StackPane pagePlaceholder; + + @FXML + private StackPane sideBarPlaceholder; + + @FXML + private StackPane commandBoxPlaceholder; + + @FXML + private StackPane resultDisplayPlaceholder; + + @FXML + private final Font interRegular = Font.loadFont(Objects + .requireNonNull(this.getClass() + .getResourceAsStream("/fonts/Inter-Regular.otf")), UNUSED_FONT_SIZE); + + @FXML + private final Font interMedium = Font.loadFont(Objects + .requireNonNull(this.getClass() + .getResourceAsStream("/fonts/Inter-Medium.otf")), UNUSED_FONT_SIZE); + + @FXML + private final Font interBold = Font.loadFont(Objects + .requireNonNull(this.getClass() + .getResourceAsStream("/fonts/Inter-Bold.otf")), UNUSED_FONT_SIZE); + + /** + * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. + */ + public MainWindow(Stage primaryStage, Logic logic, Model model) { + super(FXML, primaryStage); + + // Set dependencies + this.primaryStage = primaryStage; + this.logic = logic; + this.model = model; + + // Configure the UI + setWindowDefaultSize(logic.getGuiSettings()); + } + + /** + * Gets the primary stage. Used to show fatal errors. + * + * @return the primary stage + */ + public Stage getPrimaryStage() { + return primaryStage; + } + + /** + * Fills up all the placeholders of this window. + */ + void fillInnerParts() { + resultDisplay = new ResultDisplay(); + resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); + + CommandBox commandBox = new CommandBox(this::executeCommand); + commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + + sideBar = new SideBar(); + sideBarPlaceholder.getChildren().add(sideBar.getRoot()); + + helpPage = new HelpPage(resultDisplay, logic.getCommandHelpMessageList()); + overviewPage = new OverviewPage(logic.getFilteredPersonList(), + logic.getFilteredJobList(), model); + pageSwitcher = new PageSwitcher(this, sideBar); + sideBar.setPageSwitcher(pageSwitcher); + loadOverviewPage(); + } + + /** + * Sets the default size based on {@code guiSettings}. + */ + private void setWindowDefaultSize(GuiSettings guiSettings) { + primaryStage.setHeight(guiSettings.getWindowHeight()); + primaryStage.setWidth(guiSettings.getWindowWidth()); + if (guiSettings.getWindowCoordinates() != null) { + primaryStage.setX(guiSettings.getWindowCoordinates().getX()); + primaryStage.setY(guiSettings.getWindowCoordinates().getY()); + } + } + + void show() { + primaryStage.show(); + } + + /** + * Loads the page on the right side of the app + * + * @param page to be loaded + */ + private void loadPage(Page page) { + bp.setCenter(page.getRoot()); + } + + /** + * Swaps the currently displayed page with the Overview page + */ + public void loadOverviewPage() { + loadPage(overviewPage); + } + + /** + * Swaps the currently displayed page with the Help page + */ + public void loadHelpPage() { + loadPage(helpPage); + } + + /** + * Closes the application. + */ + @FXML + public void handleExit() { + GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), + (int) primaryStage.getX(), (int) primaryStage.getY()); + logic.setGuiSettings(guiSettings); + primaryStage.hide(); + } + + /** + * Executes the command and returns the result. + * Sends the result to PageSwitcher to switch the highlighted bar. + * + * @see peoplesoft.logic.Logic#execute(String) + */ + private CommandResult executeCommand(String commandText) throws CommandException, ParseException { + try { + CommandResult commandResult = logic.execute(commandText); + logger.info("Result: " + commandResult.getFeedbackToUser()); + resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + + if (commandResult.isShowHelp()) { + pageSwitcher.switchOnCommand(PageSwitcher.PageValues.HELP); + } else if (commandResult.isExit()) { + pageSwitcher.switchOnCommand(PageSwitcher.PageValues.EXIT); + } else { // Future Developments: if there are more pages, add them here. + pageSwitcher.switchOnCommand(PageSwitcher.PageValues.OVERVIEW); + } + + return commandResult; + } catch (CommandException | ParseException e) { + logger.info("Invalid command: " + commandText); + resultDisplay.setFeedbackToUser(e.getMessage()); + throw e; + } + } +} diff --git a/src/main/java/peoplesoft/ui/PageSwitcher.java b/src/main/java/peoplesoft/ui/PageSwitcher.java new file mode 100644 index 00000000000..3c73e6dd284 --- /dev/null +++ b/src/main/java/peoplesoft/ui/PageSwitcher.java @@ -0,0 +1,67 @@ +package peoplesoft.ui; + +import peoplesoft.ui.regions.SideBar; + +/** + * A relation class between mainwindow and sidebar which handles the page switching + * + * responsible for changing sidebar's colours and listening to commands from mainwindow and switching in main window + */ +public class PageSwitcher { + private MainWindow mw; + private SideBar sb; + + public enum PageValues { + OVERVIEW, HELP, EXIT + } + + /** + * Creates the PageSwitcher association class. + * + * @param mainW the main window which will update the page. + * @param sideB the sidebar which will update the indicated page. + */ + public PageSwitcher(MainWindow mainW, SideBar sideB) { + mw = mainW; + sb = sideB; + } + + /** + * Decides which page to switch to. Only takes in enums to guard the input. + * + * @param p the page that will be switched to + * @throws IllegalArgumentException when an invalid page is passed + */ + public void switchOnCommand(PageValues p) throws IllegalArgumentException { + assert p != null; + switch(p) { + case OVERVIEW: + loadOverviewPage(); + break; + case HELP: + loadHelpPage(); + break; + case EXIT: + exitApp(); + break; + default: + throw new IllegalArgumentException("User attempted to switch to an invalid page."); + } + + } + + private void loadOverviewPage() { + sb.activateOverviewButton(); + mw.loadOverviewPage(); + } + + private void loadHelpPage() { + sb.activateHelpButton(); + mw.loadHelpPage(); + } + + private void exitApp() { + sb.activateExitButton(); + mw.handleExit(); + } +} diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/peoplesoft/ui/Ui.java similarity index 86% rename from src/main/java/seedu/address/ui/Ui.java rename to src/main/java/peoplesoft/ui/Ui.java index 17aa0b494fe..372b1f6aa0b 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/peoplesoft/ui/Ui.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package peoplesoft.ui; import javafx.stage.Stage; diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/peoplesoft/ui/UiManager.java similarity index 83% rename from src/main/java/seedu/address/ui/UiManager.java rename to src/main/java/peoplesoft/ui/UiManager.java index fdf024138bc..f2243818ba6 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/peoplesoft/ui/UiManager.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package peoplesoft.ui; import java.util.logging.Logger; @@ -7,10 +7,11 @@ import javafx.scene.control.Alert.AlertType; import javafx.scene.image.Image; import javafx.stage.Stage; -import seedu.address.MainApp; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; +import peoplesoft.MainApp; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.commons.util.StringUtil; +import peoplesoft.logic.Logic; +import peoplesoft.model.Model; /** * The manager of the UI component. @@ -20,16 +21,18 @@ 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/logo/Logo32.png"; private Logic logic; + private Model model; private MainWindow mainWindow; /** * Creates a {@code UiManager} with the given {@code Logic}. */ - public UiManager(Logic logic) { + public UiManager(Logic logic, Model model) { this.logic = logic; + this.model = model; } @Override @@ -40,7 +43,7 @@ public void start(Stage primaryStage) { primaryStage.getIcons().add(getImage(ICON_APPLICATION)); try { - mainWindow = new MainWindow(primaryStage, logic); + mainWindow = new MainWindow(primaryStage, logic, model); mainWindow.show(); //This should be called before creating other UI parts mainWindow.fillInnerParts(); @@ -65,7 +68,7 @@ void showAlertDialogAndWait(Alert.AlertType type, String title, String headerTex private static void showAlertDialogAndWait(Stage owner, AlertType type, String title, String headerText, String contentText) { final Alert alert = new Alert(type); - alert.getDialogPane().getStylesheets().add("view/DarkTheme.css"); + alert.getDialogPane().getStylesheets().add("styles/DarkTheme.css"); alert.initOwner(owner); alert.setTitle(title); alert.setHeaderText(headerText); diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/peoplesoft/ui/UiPart.java similarity index 97% rename from src/main/java/seedu/address/ui/UiPart.java rename to src/main/java/peoplesoft/ui/UiPart.java index fc820e01a9c..9ef47bf2aa1 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/peoplesoft/ui/UiPart.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package peoplesoft.ui; import static java.util.Objects.requireNonNull; @@ -6,7 +6,7 @@ import java.net.URL; import javafx.fxml.FXMLLoader; -import seedu.address.MainApp; +import peoplesoft.MainApp; /** * Represents a distinct part of the UI. e.g. Windows, dialogs, panels, status bars, etc. diff --git a/src/main/java/peoplesoft/ui/controls/PeoplesoftTablePane.java b/src/main/java/peoplesoft/ui/controls/PeoplesoftTablePane.java new file mode 100644 index 00000000000..056ee809791 --- /dev/null +++ b/src/main/java/peoplesoft/ui/controls/PeoplesoftTablePane.java @@ -0,0 +1,53 @@ +package peoplesoft.ui.controls; + +import java.io.IOException; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.control.Label; +import javafx.scene.control.TableView; +import javafx.scene.control.TableView.TableViewFocusModel; +import javafx.scene.layout.VBox; +import peoplesoft.ui.util.TableNoSelectionModel; + +public class PeoplesoftTablePane extends VBox { + @FXML + private TableView table; + + @FXML + private Label label; + /** + * Instantiates a new instance of {@code PeoplesoftTable}. + */ + public PeoplesoftTablePane() { + FXMLLoader fxmlLoader = new FXMLLoader(PeoplesoftTablePane.class.getResource("/view/PeoplesoftTablePane.fxml")); + fxmlLoader.setRoot(this); + fxmlLoader.setController(this); + + try { + fxmlLoader.load(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + table.setSelectionModel(new TableNoSelectionModel<>(table)); + table.setFocusModel(new TableViewFocusModel<>(table)); + } + + public final void setItems(ObservableList items) { + table.setItems(items); + } + + public final TableView getTable() { + return table; + } + + public final void setLabel(String text) { + label.setText(text); + } + + public final String getLabel() { + return label.getText(); + } +} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/peoplesoft/ui/regions/CommandBox.java similarity index 88% rename from src/main/java/seedu/address/ui/CommandBox.java rename to src/main/java/peoplesoft/ui/regions/CommandBox.java index 9e75478664b..45278e9eae0 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/peoplesoft/ui/regions/CommandBox.java @@ -1,12 +1,13 @@ -package seedu.address.ui; +package peoplesoft.ui.regions; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; import javafx.scene.layout.Region; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; +import peoplesoft.logic.commands.CommandResult; +import peoplesoft.logic.commands.exceptions.CommandException; +import peoplesoft.logic.parser.exceptions.ParseException; +import peoplesoft.ui.UiPart; /** * The UI component that is responsible for receiving user command inputs. @@ -77,7 +78,7 @@ public interface CommandExecutor { /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see peoplesoft.logic.Logic#execute(String) */ CommandResult execute(String commandText) throws CommandException, ParseException; } diff --git a/src/main/java/peoplesoft/ui/regions/JobCard.java b/src/main/java/peoplesoft/ui/regions/JobCard.java new file mode 100644 index 00000000000..a64f0600db6 --- /dev/null +++ b/src/main/java/peoplesoft/ui/regions/JobCard.java @@ -0,0 +1,119 @@ +package peoplesoft.ui.regions; + +import java.util.List; +import java.util.Objects; + +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.text.TextAlignment; +import peoplesoft.model.job.Job; +import peoplesoft.model.person.Person; +import peoplesoft.ui.UiPart; + +/** + * A UI component that displays information of a {@code Job}. + */ +public class JobCard extends UiPart { + + private static final String FXML = "JobListCard.fxml"; + + public final Job job; + + private final Image cross = new Image(Objects.requireNonNull(this.getClass() + .getResourceAsStream("/images/apple-cross-emoji.png"))); + private final Image tick = new Image(Objects.requireNonNull(this.getClass() + .getResourceAsStream("/images/apple-tick-emoji.png"))); + + @FXML + private HBox cardPane; + @FXML + private Label idx; // displayed index, not job ID + @FXML + private Label desc; + @FXML + private Label duration; + @FXML + private StackPane doneIconCol; + @FXML + private ImageView doneIcon; // false + @FXML + private ImageView paidForIcon; // false + @FXML + private StackPane paidForIconCol; + @FXML + private FlowPane assigned; + + /** + * Creates a {@code PersonCode} with the given {@code Person} and index to display. + */ + public JobCard(Job job, int displayedIndex, List assignedPeople, + List colWidths) { + super(FXML); + + // dynamically size the card + List cols = List.of(idx, desc, duration, doneIconCol, + paidForIconCol, assigned); + if (cols.size() != colWidths.size()) { + throw new RuntimeException("JobCard colWidths count != cols count"); + } + for (int i = 0; i < cols.size(); i++) { + cols.get(i).minWidthProperty().bind(colWidths.get(i)); + cols.get(i).maxWidthProperty().bind(colWidths.get(i)); + } + + // calculate duration to display + int hH = job.getDuration().toHoursPart(); + int mins = job.getDuration().toMinutesPart(); + String mM = mins == 0 ? "" : (mins + "m"); + + // assign values + this.job = job; + idx.setText(displayedIndex + ""); + desc.setText(job.getDesc()); + duration.setText(String.format("%dh %s", hH, mM)); + doneIcon.setImage(job.hasPaid() ? tick : cross); + paidForIcon.setImage(job.isFinal() ? tick : cross); + + // Display people assigned to the job + ReadOnlyDoubleProperty asgnPaneWidthProperty = assigned.widthProperty(); + ObservableList visibleAssignemnts = assigned.getChildren(); + + assignedPeople.forEach(person -> { + Label lbl = new Label(person.getName() + ""); + lbl.setWrapText(true); + lbl.setTextAlignment(TextAlignment.LEFT); + lbl.maxWidthProperty().bind(asgnPaneWidthProperty); + visibleAssignemnts.add(lbl); + }); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof JobCard)) { + return false; + } + + // state check + JobCard card = (JobCard) other; + assert idx != null; + assert card.idx != null; + + return idx.getText().equals(card.idx.getText()) // same display id, not job ID + && Objects.equals(job, card.job); + } +} diff --git a/src/main/java/peoplesoft/ui/regions/JobListPanel.java b/src/main/java/peoplesoft/ui/regions/JobListPanel.java new file mode 100644 index 00000000000..7def8c3d9f5 --- /dev/null +++ b/src/main/java/peoplesoft/ui/regions/JobListPanel.java @@ -0,0 +1,74 @@ +package peoplesoft.ui.regions; + +import java.util.List; +import java.util.logging.Logger; + +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.model.Model; +import peoplesoft.model.employment.Employment; +import peoplesoft.model.job.Job; +import peoplesoft.model.person.Person; +import peoplesoft.ui.UiPart; +import peoplesoft.ui.util.NoFocusModel; +import peoplesoft.ui.util.NoSelectionModel; + +/** + * Panel containing the list of persons. + */ +public class JobListPanel extends UiPart { + private static final String FXML = "JobListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(JobListPanel.class); + private final Model model; + private final ObservableList personList; + + private List colWidths; + + @FXML + private ListView jobListView; + + /** + * Creates a {@code JobListPanel} with the given {@code ObservableList}. + */ + public JobListPanel(ObservableList jobList, ObservableList personList, + Model model, List colWidths) { + super(FXML); + this.colWidths = colWidths; + jobListView.setSelectionModel(new NoSelectionModel<>()); + jobListView.setFocusModel(new NoFocusModel<>()); + jobListView.setItems(jobList); + jobListView.setCellFactory(listView -> new JobListViewCell()); + this.personList = personList; + this.model = model; + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Job} using a {@code Job}. + */ + class JobListViewCell extends ListCell { + @Override + protected void updateItem(Job job, boolean empty) { + super.updateItem(job, empty); + + if (empty || job == null) { + setGraphic(null); + setText(null); + } else { + // Future Development: For better UX, add a new divider before each new card. + // + + // find the people assigned to this job + List peopleAssigned = Employment.getInstance().getPersons(job, model); + + setGraphic(new JobCard(job, getIndex() + 1, peopleAssigned, colWidths).getRoot()); + } + } + } + +} diff --git a/src/main/java/peoplesoft/ui/regions/PersonCard.java b/src/main/java/peoplesoft/ui/regions/PersonCard.java new file mode 100644 index 00000000000..b02421d6a99 --- /dev/null +++ b/src/main/java/peoplesoft/ui/regions/PersonCard.java @@ -0,0 +1,99 @@ +package peoplesoft.ui.regions; + +import java.util.Comparator; +import java.util.List; + +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.text.TextAlignment; +import peoplesoft.model.person.Person; +import peoplesoft.ui.UiPart; + +/** + * An UI component that displays information of a {@code Person}. + */ +public class PersonCard extends UiPart { + + private static final String FXML = "PersonListCard.fxml"; + + public final Person person; + + @FXML + private HBox cardPane; + @FXML + private Label idx; + @FXML + private Label name; + @FXML + private Label amtDue; + @FXML + private FlowPane tags; + @FXML + private Label phone; + @FXML + private Label basePay; + @FXML + private Label email; + @FXML + private Label address; + + /** + * Creates a {@code PersonCode} with the given {@code Person} and index to display. + */ + public PersonCard(Person person, int displayedIndex, List colWidths) { + super(FXML); + + List cols = List.of(idx, name, amtDue, tags, phone, basePay, email, address); + if (cols.size() != colWidths.size()) { + throw new RuntimeException("JobCard colWidths count != cols count"); + } + for (int i = 0; i < cols.size(); i++) { + cols.get(i).minWidthProperty().bind(colWidths.get(i)); + cols.get(i).maxWidthProperty().bind(colWidths.get(i)); + } + + this.person = person; + idx.setText(displayedIndex + ""); + name.setText(person.getName().fullName); + amtDue.setText(person.getAmountDue().toString()); + basePay.setText(person.getRate().toString()); + phone.setText(person.getPhone().value); + email.setText(person.getEmail().value); + address.setText(person.getAddress().value); + ReadOnlyDoubleProperty tagPaneWidthProperty = tags.widthProperty(); + ObservableList visibleTags = tags.getChildren(); + person.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> { + Label lbl = new Label(tag.tagName); + lbl.setWrapText(true); + lbl.setTextAlignment(TextAlignment.LEFT); + lbl.maxWidthProperty().bind(tagPaneWidthProperty); + visibleTags.add(lbl); + }); + } + + @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 idx.getText().equals(card.idx.getText()) + && person.equals(card.person); + } +} diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/peoplesoft/ui/regions/PersonListPanel.java similarity index 59% rename from src/main/java/seedu/address/ui/PersonListPanel.java rename to src/main/java/peoplesoft/ui/regions/PersonListPanel.java index f4c501a897b..c6ec81c91da 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/peoplesoft/ui/regions/PersonListPanel.java @@ -1,14 +1,19 @@ -package seedu.address.ui; +package peoplesoft.ui.regions; +import java.util.List; import java.util.logging.Logger; +import javafx.beans.property.ReadOnlyDoubleProperty; 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; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.model.person.Person; +import peoplesoft.ui.UiPart; +import peoplesoft.ui.util.NoFocusModel; +import peoplesoft.ui.util.NoSelectionModel; /** * Panel containing the list of persons. @@ -17,14 +22,19 @@ public class PersonListPanel extends UiPart { private static final String FXML = "PersonListPanel.fxml"; private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); + private List colWidths; + @FXML private ListView personListView; /** * Creates a {@code PersonListPanel} with the given {@code ObservableList}. */ - public PersonListPanel(ObservableList personList) { + public PersonListPanel(ObservableList personList, List colWidths) { super(FXML); + this.colWidths = colWidths; + personListView.setSelectionModel(new NoSelectionModel<>()); + personListView.setFocusModel(new NoFocusModel<>()); personListView.setItems(personList); personListView.setCellFactory(listView -> new PersonListViewCell()); } @@ -41,9 +51,12 @@ protected void updateItem(Person person, boolean empty) { setGraphic(null); setText(null); } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); + // Future Development: For better UX, add a new divider before each new card. + // + + setGraphic(new PersonCard(person, getIndex() + 1, colWidths).getRoot()); } } } - } diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/peoplesoft/ui/regions/ResultDisplay.java similarity index 80% rename from src/main/java/seedu/address/ui/ResultDisplay.java rename to src/main/java/peoplesoft/ui/regions/ResultDisplay.java index 7d98e84eedf..da74b4ee7f3 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/peoplesoft/ui/regions/ResultDisplay.java @@ -1,10 +1,11 @@ -package seedu.address.ui; +package peoplesoft.ui.regions; import static java.util.Objects.requireNonNull; import javafx.fxml.FXML; -import javafx.scene.control.TextArea; +import javafx.scene.control.Label; import javafx.scene.layout.Region; +import peoplesoft.ui.UiPart; /** * A ui for the status bar that is displayed at the header of the application. @@ -14,7 +15,7 @@ public class ResultDisplay extends UiPart { private static final String FXML = "ResultDisplay.fxml"; @FXML - private TextArea resultDisplay; + private Label resultDisplay; public ResultDisplay() { super(FXML); diff --git a/src/main/java/peoplesoft/ui/regions/SideBar.java b/src/main/java/peoplesoft/ui/regions/SideBar.java new file mode 100644 index 00000000000..3c97b8f1fe8 --- /dev/null +++ b/src/main/java/peoplesoft/ui/regions/SideBar.java @@ -0,0 +1,107 @@ +package peoplesoft.ui.regions; + +import java.net.URL; +import java.util.ResourceBundle; + +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Button; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Region; +import peoplesoft.ui.PageSwitcher; +import peoplesoft.ui.UiPart; + +public class SideBar extends UiPart implements Initializable { + private static final String FXML = "SideBar.fxml"; + + private static final String ACTIVE_COLOR = "#6368ff"; + private static final String INACTIVE_COLOR = "transparent"; + + private Button activePage; + + @FXML + private Button bOverview; + + @FXML + private Button bHelp; + + @FXML + private Button bExit; + + private PageSwitcher pageSwitcher; + + /** + * Creates a sidebar panel. + */ + public SideBar() { + super(FXML); + } + + /** + * Binds the page switcher to execute the page change in MainWindow. + * Exists because MainWindow cannot simultaneously create + * this SideBar and a PageSwitcher. + * + * @param ps the PageSwitcher object. + */ + public void setPageSwitcher(PageSwitcher ps) { + pageSwitcher = ps; + } + + /** + * Assigns parameters upon GUI initialisation. + * + * @param location + * @param resources + */ + @Override + public void initialize(URL location, ResourceBundle resources) { + activePage = bOverview; + } + + // --- mouse input detection --- + + @FXML + private void switchToOverview(MouseEvent event) { + pageSwitcher.switchOnCommand(PageSwitcher.PageValues.OVERVIEW); + } + + @FXML + private void switchToHelp(MouseEvent event) { + pageSwitcher.switchOnCommand(PageSwitcher.PageValues.HELP); + } + + @FXML + private void exitApp(MouseEvent event) { + pageSwitcher.switchOnCommand(PageSwitcher.PageValues.EXIT); + } + + // --- update view --- + + /** + * Makes the overview button light up and deactivates the current button. + */ + public void activateOverviewButton() { + switchButtonColor(bOverview); + } + + /** + * Makes the help button light up and deactivates the current button. + */ + public void activateHelpButton() { + switchButtonColor(bHelp); + } + + /** + * Makes the exit button light up and deactivates the current button. + */ + public void activateExitButton() { + switchButtonColor(bExit); + } + + private void switchButtonColor(Button b) { + activePage.setStyle("-fx-background-color: " + INACTIVE_COLOR); + b.setStyle("-fx-background-color: " + ACTIVE_COLOR + "; -fx-background-radius: 10;"); + activePage = b; + } +} diff --git a/src/main/java/peoplesoft/ui/scenes/HelpPage.java b/src/main/java/peoplesoft/ui/scenes/HelpPage.java new file mode 100644 index 00000000000..6015d77372e --- /dev/null +++ b/src/main/java/peoplesoft/ui/scenes/HelpPage.java @@ -0,0 +1,131 @@ +package peoplesoft.ui.scenes; + +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.text.Text; +import javafx.util.Callback; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.logic.commands.CommandHelpMessage; +import peoplesoft.ui.controls.PeoplesoftTablePane; +import peoplesoft.ui.regions.ResultDisplay; + +public class HelpPage extends Page { + + public static final String USERGUIDE_URL = + "https://ay2122s2-cs2103t-t11-4.github.io/tp/UserGuide.html"; + public static final String HELP_MESSAGE = "Open the User Guide"; + public static final String COPIED_MESSAGE = "Browser opened"; + + private static ResultDisplay display; + private static final Logger logger = LogsCenter.getLogger(HelpPage.class); + private static final String FXML = "HelpPage.fxml"; + + @FXML + private Button openInBrowserButton; + + @FXML + private Label helpMessage; + + /** + * Tutorial on how to add data to a table from + * https://medium.com/@keeptoo/adding-data-to-javafx-tableview-stepwise-df582acbae4f + */ + @FXML + private PeoplesoftTablePane helpTablePane; + + @FXML + private TableColumn command; + + @FXML + private TableColumn format; + + @FXML + private TableColumn examples; + + private Callback< + TableColumn, + TableCell> cellFactory = col -> { + TableCell cell = new TableCell<>(); + Text text = new Text(); + cell.setGraphic(text); + + text.getStyleClass().add("text"); + text.wrappingWidthProperty().bind(cell.widthProperty().subtract(20)); + text.textProperty().bind(cell.itemProperty()); + return cell; + }; + + /** + * Creates a new {@code HelpPage} + */ + public HelpPage(ResultDisplay rd, ObservableList commandHelpMessageList) { + super(FXML); + logger.fine("Opening PeopleSoft's Help page."); + helpMessage.setText(HELP_MESSAGE); + display = rd; + + command.setCellFactory(cellFactory); + command.setCellValueFactory(new PropertyValueFactory<>("command")); + format.setCellFactory(cellFactory); + format.setCellValueFactory(new PropertyValueFactory<>("format")); + examples.setCellFactory(cellFactory); + examples.setCellValueFactory(new PropertyValueFactory<>("examples")); + + helpTablePane.getTable().setItems(commandHelpMessageList); + } + + /** + * Opens the User Guide in the Browser. + * @@author adapted from https://stackoverflow.com/a/54869038/16777554 by Dave + */ + @FXML + private void openInBrowser() { + logger.fine("URL to open: " + USERGUIDE_URL); // change to fine logging + String oS = System.getProperty("os.name").toLowerCase(); + logger.fine("Operating system detected: " + oS); + + Runtime runtime = Runtime.getRuntime(); + + try { + if (oS.contains("mac")) { + logger.fine("Using 'open' on Mac to open webpage."); + Process open = runtime.exec("open " + USERGUIDE_URL); + logger.fine("Opened."); + } else if (oS.contains("nix") || oS.contains("nux")) { + logger.fine("Using 'xdg-open' on Linux to open webpage."); + Process open = runtime.exec("xdg-open " + USERGUIDE_URL); + logger.fine("Opened."); + } else if (oS.contains("win") && Desktop.isDesktopSupported()) { + logger.fine("Using Desktop.browse on Windows to open webpage."); + Desktop desktop = Desktop.getDesktop(); + desktop.browse(new URI(USERGUIDE_URL)); + } else { + String msg = "Unable to launch browser due to the OS."; + logger.warning(msg); + display.setFeedbackToUser(msg); + return; + } + logger.fine("User Guide successfully opened in browser."); + display.setFeedbackToUser(COPIED_MESSAGE); + } catch (URISyntaxException se) { + String msg = "The URL in the application was wrong. Please contact developers."; + logger.warning(msg); + display.setFeedbackToUser(msg); + } catch (IOException ie) { + String msg = "Unable to launch OS's browser. Please contact developers."; + logger.warning(msg); + display.setFeedbackToUser(msg); + } + } +} diff --git a/src/main/java/peoplesoft/ui/scenes/OverviewPage.java b/src/main/java/peoplesoft/ui/scenes/OverviewPage.java new file mode 100644 index 00000000000..57a280f6c87 --- /dev/null +++ b/src/main/java/peoplesoft/ui/scenes/OverviewPage.java @@ -0,0 +1,59 @@ +package peoplesoft.ui.scenes; + +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import peoplesoft.commons.core.LogsCenter; +import peoplesoft.model.Model; +import peoplesoft.model.job.Job; +import peoplesoft.model.person.Person; +import peoplesoft.ui.regions.JobListPanel; +import peoplesoft.ui.regions.PersonListPanel; + +public class OverviewPage extends Page { + + private static final Logger logger = LogsCenter.getLogger(HelpPage.class); + private static final String FXML = "OverviewPage.fxml"; + private PersonListPanel personListPanel; + private JobListPanel jobListPanel; + + @FXML + private HBox personListHeader; + @FXML + private HBox jobListHeader; + + @FXML + private StackPane personListPanelPlaceholder; + @FXML + private StackPane jobListPanelPlaceholder; + + /** + * Creates a {@code OverPage} with the given {@code ObservableList} + */ + public OverviewPage(ObservableList filteredPersonList, + ObservableList filteredJobList, Model model) { + super(FXML); + logger.fine("Opening PeopleSoft's Overview page."); + List personColWidths = personListHeader.getChildren().stream() + .map(node -> ((Region) node).widthProperty()) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableList()); + personListPanel = new PersonListPanel(filteredPersonList, personColWidths); + + List jobColWidths = jobListHeader.getChildren().stream() + .map(node -> ((Region) node).widthProperty()) + .collect(Collectors.toList()); + jobListPanel = new JobListPanel(filteredJobList, filteredPersonList, model, jobColWidths); + + personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + jobListPanelPlaceholder.getChildren().add(jobListPanel.getRoot()); + } +} diff --git a/src/main/java/peoplesoft/ui/scenes/Page.java b/src/main/java/peoplesoft/ui/scenes/Page.java new file mode 100644 index 00000000000..f8c27ac0694 --- /dev/null +++ b/src/main/java/peoplesoft/ui/scenes/Page.java @@ -0,0 +1,18 @@ +package peoplesoft.ui.scenes; + +import javafx.scene.layout.Region; +import peoplesoft.ui.UiPart; + +/** + * Models a page in the PeopleSoft application. + * All pages inherit from this class. + */ +public abstract class Page extends UiPart { + + /** + * Creates a {@code Page} from the {@code fxmlFileName} + */ + public Page(String fxmlFileName) { + super(fxmlFileName); + } +} diff --git a/src/main/java/peoplesoft/ui/util/NoFocusModel.java b/src/main/java/peoplesoft/ui/util/NoFocusModel.java new file mode 100644 index 00000000000..102a90b66de --- /dev/null +++ b/src/main/java/peoplesoft/ui/util/NoFocusModel.java @@ -0,0 +1,20 @@ +package peoplesoft.ui.util; + +import javafx.scene.control.FocusModel; + +/** + * inspired by + * https://stackoverflow.com/questions/20621752/javafx-make-listview-not-selectable-via-mouse#comment105729822_46186195 + */ +public class NoFocusModel extends FocusModel { + + @Override + protected int getItemCount() { + return 0; + } + + @Override + protected T getModelItem(int index) { + return null; + } +} diff --git a/src/main/java/peoplesoft/ui/util/NoSelectionModel.java b/src/main/java/peoplesoft/ui/util/NoSelectionModel.java new file mode 100644 index 00000000000..5cb57a886c9 --- /dev/null +++ b/src/main/java/peoplesoft/ui/util/NoSelectionModel.java @@ -0,0 +1,75 @@ +package peoplesoft.ui.util; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.MultipleSelectionModel; + +/** + * inspired by https://stackoverflow.com/a/46186195 + */ +public class NoSelectionModel extends MultipleSelectionModel { + + @Override + public ObservableList getSelectedIndices() { + return FXCollections.emptyObservableList(); + } + + @Override + public ObservableList getSelectedItems() { + return FXCollections.emptyObservableList(); + } + + @Override + public void selectIndices(int index, int... indices) { + } + + @Override + public void selectAll() { + } + + @Override + public void selectPrevious() { + } + + @Override + public void selectNext() { + } + + @Override + public void selectFirst() { + } + + @Override + public void selectLast() { + } + + @Override + public void clearAndSelect(int index) { + } + + @Override + public void select(int index) { + } + + @Override + public void select(T obj) { + } + + @Override + public void clearSelection(int index) { + } + + @Override + public void clearSelection() { + } + + @Override + public boolean isSelected(int index) { + return false; + } + + @Override + public boolean isEmpty() { + return true; + } +} diff --git a/src/main/java/peoplesoft/ui/util/TableNoFocusModel.java b/src/main/java/peoplesoft/ui/util/TableNoFocusModel.java new file mode 100644 index 00000000000..2b972ebcb5b --- /dev/null +++ b/src/main/java/peoplesoft/ui/util/TableNoFocusModel.java @@ -0,0 +1,25 @@ +package peoplesoft.ui.util; + +import javafx.scene.control.TableView; +import javafx.scene.control.TableView.TableViewFocusModel; + +/** + * inspired by + * https://stackoverflow.com/questions/20621752/javafx-make-listview-not-selectable-via-mouse#comment105729822_46186195 + */ +public class TableNoFocusModel extends TableViewFocusModel { + + public TableNoFocusModel(TableView tableView) { + super(tableView); + } + + @Override + protected int getItemCount() { + return 0; + } + + @Override + protected T getModelItem(int index) { + return null; + } +} diff --git a/src/main/java/peoplesoft/ui/util/TableNoSelectionModel.java b/src/main/java/peoplesoft/ui/util/TableNoSelectionModel.java new file mode 100644 index 00000000000..4710c8d15c7 --- /dev/null +++ b/src/main/java/peoplesoft/ui/util/TableNoSelectionModel.java @@ -0,0 +1,120 @@ +package peoplesoft.ui.util; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TablePosition; +import javafx.scene.control.TableView; +import javafx.scene.control.TableView.TableViewSelectionModel; + +/** + * inspired by https://stackoverflow.com/a/46186195 + */ +public class TableNoSelectionModel extends TableViewSelectionModel { + + public TableNoSelectionModel(TableView tableView) { + super(tableView); + } + + @Override + public ObservableList getSelectedIndices() { + return FXCollections.emptyObservableList(); + } + + @Override + public ObservableList getSelectedItems() { + return FXCollections.emptyObservableList(); + } + + @Override + public ObservableList getSelectedCells() { + return FXCollections.emptyObservableList(); + } + + @Override + public void selectIndices(int index, int... indices) { + } + + @Override + public void selectAll() { + } + + @Override + public void selectPrevious() { + } + + @Override + public void selectNext() { + } + + @Override + public void selectFirst() { + } + + @Override + public void selectLast() { + } + + @Override + public void selectLeftCell() { + } + + @Override + public void selectRightCell() { + } + + @Override + public void selectAboveCell() { + } + + @Override + public void selectBelowCell() { + } + + @Override + public void clearAndSelect(int index) { + } + + @Override + public void clearAndSelect(int row, TableColumn column) { + } + + @Override + public void select(int index) { + } + + @Override + public void select(T obj) { + } + + @Override + public void select(int row, TableColumn column) { + } + + @Override + public void clearSelection(int index) { + } + + @Override + public void clearSelection() { + } + + @Override + public void clearSelection(int row, TableColumn column) { + } + + @Override + public boolean isSelected(int index) { + return false; + } + + @Override + public boolean isSelected(int row, TableColumn column) { + return false; + } + + @Override + public boolean isEmpty() { + return true; + } +} diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java deleted file mode 100644 index 1deb3a1e469..00000000000 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ /dev/null @@ -1,13 +0,0 @@ -package seedu.address.commons.core; - -/** - * Container for user visible messages. - */ -public class Messages { - - public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; - public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; - -} diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/seedu/address/commons/util/JsonUtil.java deleted file mode 100644 index 8ef609f055d..00000000000 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ /dev/null @@ -1,143 +0,0 @@ -package seedu.address.commons.util; - -import static java.util.Objects.requireNonNull; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; - -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; - -/** - * Converts a Java object instance to JSON and vice versa - */ -public class JsonUtil { - - private static final Logger logger = LogsCenter.getLogger(JsonUtil.class); - - private static ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules() - .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) - .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) - .registerModule(new SimpleModule("SimpleModule") - .addSerializer(Level.class, new ToStringSerializer()) - .addDeserializer(Level.class, new LevelDeserializer(Level.class))); - - static void serializeObjectToJsonFile(Path jsonFile, T objectToSerialize) throws IOException { - FileUtil.writeToFile(jsonFile, toJsonString(objectToSerialize)); - } - - static T deserializeObjectFromJsonFile(Path jsonFile, Class classOfObjectToDeserialize) - throws IOException { - return fromJsonString(FileUtil.readFromFile(jsonFile), classOfObjectToDeserialize); - } - - /** - * Returns the Json object from the given file or {@code Optional.empty()} object if the file is not found. - * If any values are missing from the file, default values will be used, as long as the file is a valid json file. - * @param filePath cannot be null. - * @param classOfObjectToDeserialize Json file has to correspond to the structure in the class given here. - * @throws DataConversionException if the file format is not as expected. - */ - public static Optional readJsonFile( - Path filePath, Class classOfObjectToDeserialize) throws DataConversionException { - requireNonNull(filePath); - - if (!Files.exists(filePath)) { - logger.info("Json file " + filePath + " not found"); - return Optional.empty(); - } - - T jsonFile; - - try { - jsonFile = deserializeObjectFromJsonFile(filePath, classOfObjectToDeserialize); - } catch (IOException e) { - logger.warning("Error reading from jsonFile file " + filePath + ": " + e); - throw new DataConversionException(e); - } - - return Optional.of(jsonFile); - } - - /** - * Saves the Json object to the specified file. - * Overwrites existing file if it exists, creates a new file if it doesn't. - * @param jsonFile cannot be null - * @param filePath cannot be null - * @throws IOException if there was an error during writing to the file - */ - public static void saveJsonFile(T jsonFile, Path filePath) throws IOException { - requireNonNull(filePath); - requireNonNull(jsonFile); - - serializeObjectToJsonFile(filePath, jsonFile); - } - - - /** - * Converts a given string representation of a JSON data to instance of a class - * @param The generic type to create an instance of - * @return The instance of T with the specified values in the JSON string - */ - public static T fromJsonString(String json, Class instanceClass) throws IOException { - return objectMapper.readValue(json, instanceClass); - } - - /** - * Converts a given instance of a class into its JSON data string representation - * @param instance The T object to be converted into the JSON string - * @param The generic type to create an instance of - * @return JSON data representation of the given class instance, in string - */ - public static String toJsonString(T instance) throws JsonProcessingException { - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(instance); - } - - /** - * Contains methods that retrieve logging level from serialized string. - */ - private static class LevelDeserializer extends FromStringDeserializer { - - protected LevelDeserializer(Class vc) { - super(vc); - } - - @Override - protected Level _deserialize(String value, DeserializationContext ctxt) { - return getLoggingLevel(value); - } - - /** - * Gets the logging level that matches loggingLevelString - *

- * Returns null if there are no matches - * - */ - private Level getLoggingLevel(String loggingLevelString) { - return Level.parse(loggingLevelString); - } - - @Override - public Class handledType() { - return Level.class; - } - } - -} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java deleted file mode 100644 index 71656d7c5c8..00000000000 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ /dev/null @@ -1,67 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Adds a person to the address book. - */ -public class AddCommand extends Command { - - public static final String COMMAND_WORD = "add"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; - - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; - - private final Person toAdd; - - /** - * Creates an AddCommand to add the specified {@code Person} - */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, 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)); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java deleted file mode 100644 index 9c86b1fa6e4..00000000000 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ /dev/null @@ -1,23 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.model.AddressBook; -import seedu.address.model.Model; - -/** - * Clears the address book. - */ -public class ClearCommand extends Command { - - public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.setAddressBook(new AddressBook()); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java deleted file mode 100644 index 02fd256acba..00000000000 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ /dev/null @@ -1,53 +0,0 @@ -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. - */ -public class DeleteCommand extends Command { - - public static final String COMMAND_WORD = "delete"; - - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - - private final Index targetIndex; - - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, 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 - } -} 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/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java deleted file mode 100644 index bf824f91bd0..00000000000 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.address.logic.commands; - -import seedu.address.model.Model; - -/** - * Format full help instructions for every command for display. - */ -public class HelpCommand extends Command { - - public static final String COMMAND_WORD = "help"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" - + "Example: " + COMMAND_WORD; - - public static final String SHOWING_HELP_MESSAGE = "Opened help window."; - - @Override - public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java deleted file mode 100644 index 84be6ad2596..00000000000 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ /dev/null @@ -1,24 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import seedu.address.model.Model; - -/** - * Lists all persons in the address book to the user. - */ -public class ListCommand extends Command { - - public static final String COMMAND_WORD = "list"; - - public static final String MESSAGE_SUCCESS = "Listed all persons"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java deleted file mode 100644 index 3b8bfa035e8..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ /dev/null @@ -1,60 +0,0 @@ -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 java.util.Set; -import java.util.stream.Stream; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new AddCommand object - */ -public class AddCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the AddCommand - * and returns an AddCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public AddCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); - } - - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java deleted file mode 100644 index 1e466792b46..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ /dev/null @@ -1,76 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses user input. - */ -public class AddressBookParser { - - /** - * Used for initial separation of command word and args. - */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - - /** - * Parses user input into command for execution. - * - * @param userInput full user input string - * @return the command based on the user input - * @throws ParseException if the user input does not conform the expected format - */ - public Command parseCommand(String userInput) throws ParseException { - final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); - if (!matcher.matches()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); - switch (commandWord) { - - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); - - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); - - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); - - case ClearCommand.COMMAND_WORD: - return new ClearCommand(); - - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); - - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - - default: - throw new ParseException(MESSAGE_UNKNOWN_COMMAND); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java deleted file mode 100644 index 522b93081cc..00000000000 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ /dev/null @@ -1,29 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses input arguments and creates a new DeleteCommand object - */ -public class DeleteCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public DeleteCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); - } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java deleted file mode 100644 index 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/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java deleted file mode 100644 index b117acb9c55..00000000000 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ /dev/null @@ -1,124 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods used for parsing strings in the various *Parser classes. - */ -public class ParserUtil { - - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; - - /** - * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be - * trimmed. - * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). - */ - public static Index parseIndex(String oneBasedIndex) throws ParseException { - String trimmedIndex = oneBasedIndex.trim(); - if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { - throw new ParseException(MESSAGE_INVALID_INDEX); - } - return Index.fromOneBased(Integer.parseInt(trimmedIndex)); - } - - /** - * Parses a {@code String name} into a {@code Name}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code name} is invalid. - */ - public static Name parseName(String name) throws ParseException { - requireNonNull(name); - String trimmedName = name.trim(); - if (!Name.isValidName(trimmedName)) { - throw new ParseException(Name.MESSAGE_CONSTRAINTS); - } - return new Name(trimmedName); - } - - /** - * Parses a {@code String phone} into a {@code Phone}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code phone} is invalid. - */ - public static Phone parsePhone(String phone) throws ParseException { - requireNonNull(phone); - String trimmedPhone = phone.trim(); - if (!Phone.isValidPhone(trimmedPhone)) { - throw new ParseException(Phone.MESSAGE_CONSTRAINTS); - } - return new Phone(trimmedPhone); - } - - /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code address} is invalid. - */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); - } - return new Address(trimmedAddress); - } - - /** - * Parses a {@code String email} into an {@code Email}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code email} is invalid. - */ - public static Email parseEmail(String email) throws ParseException { - requireNonNull(email); - String trimmedEmail = email.trim(); - if (!Email.isValidEmail(trimmedEmail)) { - throw new ParseException(Email.MESSAGE_CONSTRAINTS); - } - return new Email(trimmedEmail); - } - - /** - * Parses a {@code String tag} into a {@code Tag}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code tag} is invalid. - */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { - throw new ParseException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(trimmedTag); - } - - /** - * Parses {@code Collection tags} into a {@code Set}. - */ - public static Set parseTags(Collection tags) throws ParseException { - requireNonNull(tags); - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); - } - return tagSet; - } -} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java deleted file mode 100644 index 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/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index 60472ca22a0..00000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,57 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String VALIDATION_REGEX = "[^\\s].*"; - - public final String value; - - /** - * Constructs an {@code Address}. - * - * @param address A valid address. - */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; - } - - /** - * Returns true if a given string is a valid email. - */ - public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Address // instanceof handles nulls - && value.equals(((Address) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java deleted file mode 100644 index f866e7133de..00000000000 --- a/src/main/java/seedu/address/model/person/Email.java +++ /dev/null @@ -1,71 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's email in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} - */ -public class Email { - - private static final String SPECIAL_CHARACTERS = "+_.-"; - public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " - + "and adhere to the following constraints:\n" - + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " - + "the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " - + "characters.\n" - + "2. This is followed by a '@' and then a domain name. The domain name is made up of domain labels " - + "separated by periods.\n" - + "The domain name must:\n" - + " - end with a domain label at least 2 characters long\n" - + " - have each domain label start and end with alphanumeric characters\n" - + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any."; - // alphanumeric and special characters - private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]+"; // alphanumeric characters except underscore - private static final String LOCAL_PART_REGEX = "^" + ALPHANUMERIC_NO_UNDERSCORE + "([" + SPECIAL_CHARACTERS + "]" - + ALPHANUMERIC_NO_UNDERSCORE + ")*"; - private static final String DOMAIN_PART_REGEX = ALPHANUMERIC_NO_UNDERSCORE - + "(-" + ALPHANUMERIC_NO_UNDERSCORE + ")*"; - private static final String DOMAIN_LAST_PART_REGEX = "(" + DOMAIN_PART_REGEX + "){2,}$"; // At least two chars - private static final String DOMAIN_REGEX = "(" + DOMAIN_PART_REGEX + "\\.)*" + DOMAIN_LAST_PART_REGEX; - public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" + DOMAIN_REGEX; - - public final String value; - - /** - * Constructs an {@code Email}. - * - * @param email A valid email address. - */ - public Email(String email) { - requireNonNull(email); - checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); - value = email; - } - - /** - * Returns if a given string is a valid email. - */ - public static boolean isValidEmail(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Email // instanceof handles nulls - && value.equals(((Email) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java deleted file mode 100644 index 79244d71cf7..00000000000 --- a/src/main/java/seedu/address/model/person/Name.java +++ /dev/null @@ -1,59 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's 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"; - - /* - * 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 final String fullName; - - /** - * Constructs a {@code Name}. - * - * @param name A valid name. - */ - public Name(String name) { - requireNonNull(name); - checkArgument(isValidName(name), MESSAGE_CONSTRAINTS); - fullName = name; - } - - /** - * Returns true if a given string is a valid name. - */ - public static boolean isValidName(String test) { - return test.matches(VALIDATION_REGEX); - } - - - @Override - public String toString() { - return fullName; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Name // instanceof handles nulls - && fullName.equals(((Name) other).fullName)); // state check - } - - @Override - public int hashCode() { - return fullName.hashCode(); - } - -} 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/Phone.java b/src/main/java/seedu/address/model/person/Phone.java deleted file mode 100644 index 872c76b382f..00000000000 --- a/src/main/java/seedu/address/model/person/Phone.java +++ /dev/null @@ -1,53 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's phone number in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} - */ -public class Phone { - - - public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String VALIDATION_REGEX = "\\d{3,}"; - public final String value; - - /** - * Constructs a {@code Phone}. - * - * @param phone A valid phone number. - */ - public Phone(String phone) { - requireNonNull(phone); - checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); - value = phone; - } - - /** - * Returns true if a given string is a valid phone number. - */ - public static boolean isValidPhone(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 Phone // instanceof handles nulls - && value.equals(((Phone) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} 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/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java deleted file mode 100644 index b0ea7e7dad7..00000000000 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ /dev/null @@ -1,54 +0,0 @@ -package seedu.address.model.tag; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Tag in the address book. - * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} - */ -public class Tag { - - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; - - public final String tagName; - - /** - * Constructs a {@code Tag}. - * - * @param tagName A valid tag name. - */ - public Tag(String tagName) { - requireNonNull(tagName); - checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); - this.tagName = tagName; - } - - /** - * Returns true if a given string is a valid tag name. - */ - public static boolean isValidTagName(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Tag // instanceof handles nulls - && tagName.equals(((Tag) other).tagName)); // state check - } - - @Override - public int hashCode() { - return tagName.hashCode(); - } - - /** - * Format state as text for viewing. - */ - public String toString() { - return '[' + tagName + ']'; - } - -} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java deleted file mode 100644 index 1806da4facf..00000000000 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.model.util; - -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods for populating {@code AddressBook} with sample data. - */ -public class SampleDataUtil { - public static Person[] getSamplePersons() { - return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) - }; - } - - public static ReadOnlyAddressBook getSampleAddressBook() { - AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); - } - return sampleAb; - } - - /** - * Returns a tag set containing the list of strings given. - */ - public static Set getTagSet(String... strings) { - return Arrays.stream(strings) - .map(Tag::new) - .collect(Collectors.toSet()); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java deleted file mode 100644 index a6321cec2ea..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ /dev/null @@ -1,109 +0,0 @@ -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.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; - -/** - * Jackson-friendly version of {@link Person}. - */ -class JsonAdaptedPerson { - - public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; - - private final String name; - private final String phone; - private final String email; - private final String address; - private final List tagged = new ArrayList<>(); - - /** - * Constructs a {@code JsonAdaptedPerson} with the given person details. - */ - @JsonCreator - public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tagged") List tagged) { - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - if (tagged != null) { - this.tagged.addAll(tagged); - } - } - - /** - * Converts a given {@code Person} into this class for Jackson use. - */ - public JsonAdaptedPerson(Person source) { - name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - 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. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted person. - */ - public Person toModelType() throws IllegalValueException { - final List personTags = new ArrayList<>(); - for (JsonAdaptedTag tag : tagged) { - personTags.add(tag.toModelType()); - } - - if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); - } - if (!Name.isValidName(name)) { - throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); - } - final Name modelName = new Name(name); - - if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { - throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); - } - final Phone modelPhone = new Phone(phone); - - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { - throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); - } - final Email modelEmail = new Email(email); - - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); - } - final Address modelAddress = new Address(address); - - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/address/storage/JsonAdaptedTag.java deleted file mode 100644 index 0df22bdb754..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ /dev/null @@ -1,48 +0,0 @@ -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.tag.Tag; - -/** - * Jackson-friendly version of {@link Tag}. - */ -class JsonAdaptedTag { - - private final String tagName; - - /** - * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. - */ - @JsonCreator - public JsonAdaptedTag(String tagName) { - this.tagName = tagName; - } - - /** - * Converts a given {@code Tag} into this class for Jackson use. - */ - public JsonAdaptedTag(Tag source) { - tagName = source.tagName; - } - - @JsonValue - public String getTagName() { - return tagName; - } - - /** - * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted tag. - */ - public Tag toModelType() throws IllegalValueException { - if (!Tag.isValidTagName(tagName)) { - throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(tagName); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java deleted file mode 100644 index 5efd834091d..00000000000 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * An Immutable AddressBook that is serializable to JSON format. - */ -@JsonRootName(value = "addressbook") -class JsonSerializableAddressBook { - - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; - - private final List persons = new ArrayList<>(); - - /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. - */ - @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { - this.persons.addAll(persons); - } - - /** - * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. - * - * @param source future changes to this will not affect the created {@code JsonSerializableAddressBook}. - */ - public JsonSerializableAddressBook(ReadOnlyAddressBook source) { - persons.addAll(source.getPersonList().stream().map(JsonAdaptedPerson::new).collect(Collectors.toList())); - } - - /** - * Converts this address book into the model's {@code AddressBook} object. - * - * @throws IllegalValueException if there were any data constraints violated. - */ - public AddressBook toModelType() throws IllegalValueException { - AddressBook addressBook = new AddressBook(); - for (JsonAdaptedPerson jsonAdaptedPerson : persons) { - Person person = jsonAdaptedPerson.toModelType(); - if (addressBook.hasPerson(person)) { - throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); - } - addressBook.addPerson(person); - } - return addressBook; - } - -} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java deleted file mode 100644 index 9a665915949..00000000000 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ /dev/null @@ -1,102 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -import javafx.fxml.FXML; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.input.Clipboard; -import javafx.scene.input.ClipboardContent; -import javafx.stage.Stage; -import seedu.address.commons.core.LogsCenter; - -/** - * Controller for a help page - */ -public class HelpWindow extends UiPart { - - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; - public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; - - private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); - private static final String FXML = "HelpWindow.fxml"; - - @FXML - private Button copyButton; - - @FXML - private Label helpMessage; - - /** - * Creates a new HelpWindow. - * - * @param root Stage to use as the root of the HelpWindow. - */ - public HelpWindow(Stage root) { - super(FXML, root); - helpMessage.setText(HELP_MESSAGE); - } - - /** - * Creates a new HelpWindow. - */ - public HelpWindow() { - this(new Stage()); - } - - /** - * Shows the help window. - * @throws IllegalStateException - *

    - *
  • - * if this method is called on a thread other than the JavaFX Application Thread. - *
  • - *
  • - * if this method is called during animation or layout processing. - *
  • - *
  • - * if this method is called on the primary stage. - *
  • - *
  • - * if {@code dialogStage} is already showing. - *
  • - *
- */ - public void show() { - logger.fine("Showing help page about the application."); - getRoot().show(); - getRoot().centerOnScreen(); - } - - /** - * Returns true if the help window is currently being shown. - */ - public boolean isShowing() { - return getRoot().isShowing(); - } - - /** - * Hides the help window. - */ - public void hide() { - getRoot().hide(); - } - - /** - * Focuses on the help window. - */ - public void focus() { - getRoot().requestFocus(); - } - - /** - * Copies the URL to the user guide to the clipboard. - */ - @FXML - private void copyUrl() { - final Clipboard clipboard = Clipboard.getSystemClipboard(); - final ClipboardContent url = new ClipboardContent(); - url.putString(USERGUIDE_URL); - clipboard.setContent(url); - } -} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java deleted file mode 100644 index 9106c3aa6e5..00000000000 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ /dev/null @@ -1,196 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -import javafx.event.ActionEvent; -import javafx.fxml.FXML; -import javafx.scene.control.MenuItem; -import javafx.scene.control.TextInputControl; -import javafx.scene.input.KeyCombination; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.StackPane; -import javafx.stage.Stage; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.Logic; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * The Main Window. Provides the basic application layout containing - * a menu bar and space where other JavaFX elements can be placed. - */ -public class MainWindow extends UiPart { - - private static final String FXML = "MainWindow.fxml"; - - private final Logger logger = LogsCenter.getLogger(getClass()); - - private Stage primaryStage; - private Logic logic; - - // Independent Ui parts residing in this Ui container - private PersonListPanel personListPanel; - private ResultDisplay resultDisplay; - private HelpWindow helpWindow; - - @FXML - private StackPane commandBoxPlaceholder; - - @FXML - private MenuItem helpMenuItem; - - @FXML - private StackPane personListPanelPlaceholder; - - @FXML - private StackPane resultDisplayPlaceholder; - - @FXML - private StackPane statusbarPlaceholder; - - /** - * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. - */ - public MainWindow(Stage primaryStage, Logic logic) { - super(FXML, primaryStage); - - // Set dependencies - this.primaryStage = primaryStage; - this.logic = logic; - - // Configure the UI - setWindowDefaultSize(logic.getGuiSettings()); - - setAccelerators(); - - helpWindow = new HelpWindow(); - } - - public Stage getPrimaryStage() { - return primaryStage; - } - - private void setAccelerators() { - setAccelerator(helpMenuItem, KeyCombination.valueOf("F1")); - } - - /** - * Sets the accelerator of a MenuItem. - * @param keyCombination the KeyCombination value of the accelerator - */ - private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { - menuItem.setAccelerator(keyCombination); - - /* - * TODO: the code below can be removed once the bug reported here - * https://bugs.openjdk.java.net/browse/JDK-8131666 - * is fixed in later version of SDK. - * - * According to the bug report, TextInputControl (TextField, TextArea) will - * consume function-key events. Because CommandBox contains a TextField, and - * ResultDisplay contains a TextArea, thus some accelerators (e.g F1) will - * not work when the focus is in them because the key event is consumed by - * the TextInputControl(s). - * - * For now, we add following event filter to capture such key events and open - * help window purposely so to support accelerators even when focus is - * in CommandBox or ResultDisplay. - */ - getRoot().addEventFilter(KeyEvent.KEY_PRESSED, event -> { - if (event.getTarget() instanceof TextInputControl && keyCombination.match(event)) { - menuItem.getOnAction().handle(new ActionEvent()); - event.consume(); - } - }); - } - - /** - * Fills up all the placeholders of this window. - */ - void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); - - resultDisplay = new ResultDisplay(); - resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); - statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - - CommandBox commandBox = new CommandBox(this::executeCommand); - commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); - } - - /** - * Sets the default size based on {@code guiSettings}. - */ - private void setWindowDefaultSize(GuiSettings guiSettings) { - primaryStage.setHeight(guiSettings.getWindowHeight()); - primaryStage.setWidth(guiSettings.getWindowWidth()); - if (guiSettings.getWindowCoordinates() != null) { - primaryStage.setX(guiSettings.getWindowCoordinates().getX()); - primaryStage.setY(guiSettings.getWindowCoordinates().getY()); - } - } - - /** - * Opens the help window or focuses on it if it's already opened. - */ - @FXML - public void handleHelp() { - if (!helpWindow.isShowing()) { - helpWindow.show(); - } else { - helpWindow.focus(); - } - } - - void show() { - primaryStage.show(); - } - - /** - * Closes the application. - */ - @FXML - private void handleExit() { - GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), - (int) primaryStage.getX(), (int) primaryStage.getY()); - logic.setGuiSettings(guiSettings); - helpWindow.hide(); - primaryStage.hide(); - } - - public PersonListPanel getPersonListPanel() { - return personListPanel; - } - - /** - * Executes the command and returns the result. - * - * @see seedu.address.logic.Logic#execute(String) - */ - private CommandResult executeCommand(String commandText) throws CommandException, ParseException { - try { - CommandResult commandResult = logic.execute(commandText); - logger.info("Result: " + commandResult.getFeedbackToUser()); - resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); - - if (commandResult.isShowHelp()) { - handleHelp(); - } - - if (commandResult.isExit()) { - handleExit(); - } - - return commandResult; - } catch (CommandException | ParseException e) { - logger.info("Invalid command: " + commandText); - resultDisplay.setFeedbackToUser(e.getMessage()); - 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/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java deleted file mode 100644 index b577f829423..00000000000 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ /dev/null @@ -1,28 +0,0 @@ -package seedu.address.ui; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.layout.Region; - -/** - * A ui for the status bar that is displayed at the footer of the application. - */ -public class StatusBarFooter extends UiPart { - - private static final String FXML = "StatusBarFooter.fxml"; - - @FXML - private Label saveLocationStatus; - - /** - * Creates a {@code StatusBarFooter} with the given {@code Path}. - */ - public StatusBarFooter(Path saveLocation) { - super(FXML); - saveLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); - } - -} diff --git a/src/main/resources/fonts/Inter-Bold.otf b/src/main/resources/fonts/Inter-Bold.otf new file mode 100644 index 00000000000..c74cc0c6c13 Binary files /dev/null and b/src/main/resources/fonts/Inter-Bold.otf differ diff --git a/src/main/resources/fonts/Inter-Medium.otf b/src/main/resources/fonts/Inter-Medium.otf new file mode 100644 index 00000000000..ca7bfcd4340 Binary files /dev/null and b/src/main/resources/fonts/Inter-Medium.otf differ diff --git a/src/main/resources/fonts/Inter-Regular.otf b/src/main/resources/fonts/Inter-Regular.otf new file mode 100644 index 00000000000..84e6a61c3c0 Binary files /dev/null and b/src/main/resources/fonts/Inter-Regular.otf differ diff --git a/src/main/resources/fonts/LICENSE.txt b/src/main/resources/fonts/LICENSE.txt new file mode 100644 index 00000000000..ff80f8c6156 --- /dev/null +++ b/src/main/resources/fonts/LICENSE.txt @@ -0,0 +1,94 @@ +Copyright (c) 2016-2020 The Inter Project Authors. +"Inter" is trademark of Rasmus Andersson. +https://github.com/rsms/inter + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png deleted file mode 100644 index 29810cf1fd9..00000000000 Binary files a/src/main/resources/images/address_book_32.png and /dev/null differ diff --git a/src/main/resources/images/apple-cross-emoji.png b/src/main/resources/images/apple-cross-emoji.png new file mode 100644 index 00000000000..b678d102d00 Binary files /dev/null and b/src/main/resources/images/apple-cross-emoji.png differ diff --git a/src/main/resources/images/apple-tick-emoji.png b/src/main/resources/images/apple-tick-emoji.png new file mode 100644 index 00000000000..6c6cfe150ac Binary files /dev/null and b/src/main/resources/images/apple-tick-emoji.png differ diff --git a/src/main/resources/images/command.png b/src/main/resources/images/command.png new file mode 100644 index 00000000000..553aca3ae58 Binary files /dev/null and b/src/main/resources/images/command.png differ diff --git a/src/main/resources/images/commandLarge.png b/src/main/resources/images/commandLarge.png new file mode 100644 index 00000000000..7f2458e3370 Binary files /dev/null and b/src/main/resources/images/commandLarge.png differ diff --git a/src/main/resources/images/logo/Logo128.png b/src/main/resources/images/logo/Logo128.png new file mode 100644 index 00000000000..865ab225ee5 Binary files /dev/null and b/src/main/resources/images/logo/Logo128.png differ diff --git a/src/main/resources/images/logo/Logo16.png b/src/main/resources/images/logo/Logo16.png new file mode 100644 index 00000000000..341234a07e5 Binary files /dev/null and b/src/main/resources/images/logo/Logo16.png differ diff --git a/src/main/resources/images/logo/Logo256.png b/src/main/resources/images/logo/Logo256.png new file mode 100644 index 00000000000..d77dab21237 Binary files /dev/null and b/src/main/resources/images/logo/Logo256.png differ diff --git a/src/main/resources/images/logo/Logo32.png b/src/main/resources/images/logo/Logo32.png new file mode 100644 index 00000000000..f6d853eaeff Binary files /dev/null and b/src/main/resources/images/logo/Logo32.png differ diff --git a/src/main/resources/images/logo/Logo512.png b/src/main/resources/images/logo/Logo512.png new file mode 100644 index 00000000000..02e6bb9f6e3 Binary files /dev/null and b/src/main/resources/images/logo/Logo512.png differ diff --git a/src/main/resources/images/logo/Logo64.png b/src/main/resources/images/logo/Logo64.png new file mode 100644 index 00000000000..4df7dc8fc67 Binary files /dev/null and b/src/main/resources/images/logo/Logo64.png differ diff --git a/src/main/resources/images/open_browser.png b/src/main/resources/images/open_browser.png new file mode 100644 index 00000000000..6666ea11cd5 Binary files /dev/null and b/src/main/resources/images/open_browser.png differ diff --git a/src/main/resources/images/sidebar/exit.png b/src/main/resources/images/sidebar/exit.png new file mode 100644 index 00000000000..e935fa64b7a Binary files /dev/null and b/src/main/resources/images/sidebar/exit.png differ diff --git a/src/main/resources/images/sidebar/help.png b/src/main/resources/images/sidebar/help.png new file mode 100644 index 00000000000..6f58336ec01 Binary files /dev/null and b/src/main/resources/images/sidebar/help.png differ diff --git a/src/main/resources/images/sidebar/overview.png b/src/main/resources/images/sidebar/overview.png new file mode 100644 index 00000000000..41712460e7c Binary files /dev/null and b/src/main/resources/images/sidebar/overview.png differ diff --git a/src/main/resources/images/sidebar/settings.png b/src/main/resources/images/sidebar/settings.png new file mode 100644 index 00000000000..18a899cf0af Binary files /dev/null and b/src/main/resources/images/sidebar/settings.png differ diff --git a/src/main/resources/images/sidebar/toplogo.png b/src/main/resources/images/sidebar/toplogo.png new file mode 100644 index 00000000000..83ebac9db32 Binary files /dev/null and b/src/main/resources/images/sidebar/toplogo.png differ diff --git a/src/main/resources/styles/DarkTheme.css b/src/main/resources/styles/DarkTheme.css new file mode 100644 index 00000000000..8d409adbc34 --- /dev/null +++ b/src/main/resources/styles/DarkTheme.css @@ -0,0 +1,245 @@ +/* Dark theme for PeopleSoft */ +/* Using HTTP Status Design Web App by uikit */ +/* Retrieved from https://uikit.to/http-status-design-web-app/ */ + +.root { + /* JavaFX CSS Parser only supports + custom variables for colours and nothing else */ + /* Source: https://stackoverflow.com/questions/13566210/declaring-variable-in-javafx-css-file */ + /* Docs https://docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.html */ + + base: white; + + list-cell-even: #cdcdcd; + list-cell-odd: #dedede; + list-cell-selected: derive(list-cell-even, -10%); + selected: accent; + + /* Font Colors */ + text-default: black; + text-inverse: white; + text-error: #FF5736; + -fx-focus-color: black; + + /* Final Colors */ + text-norm: white; + text-faded: #858AAB; + + bg: #1B1A2B; + pane: #201f31; + pane-light: #2e2d42; + accent: #6368ff; + + tag: #262645; + tag-green: #249b7e; + tag-green-bg: #202a38; + + /* Unused */ + tag-yellow: #FF9860; + tag-yellow-bg: #382F3C; + tag-red: #E06767; + tag-red-bg: #352A3D; +} + +.background { + -fx-background-color: bg; +} + +.table-view { + -fx-base: base; + -fx-control-inner-background: base; + -fx-background-color: base; + -fx-padding: 5; +} + +.table-view .column-header-background { + -fx-background-color: transparent; +} + +.table-view .column-header, .table-view .filler { + -fx-size: 35; + -fx-border-width: 0 0 1 0; + -fx-background-color: transparent; + -fx-border-color: + transparent + transparent + transparent + transparent; + -fx-border-insets: 0 10 1 0; +} + +/* used for help page table tho.. */ +.table-view .column-header .label { + -fx-font-family: "Inter Bold"; + -fx-font-size: 20pt; + -fx-text-fill: text-default; + -fx-alignment: center-left; + -fx-opacity: 1; +} + +.table-view:focused .table-row-cell:filled:focused:selected { + -fx-background-color: -fx-focus-color; +} + +.split-pane:horizontal .split-pane-divider { + -fx-background-color: derive(base, 20%); + -fx-border-color: transparent transparent transparent section-border; +} + +.split-pane { + -fx-border-radius: 1; + -fx-border-width: 1; + -fx-background-color: derive(base, 20%); +} + +.list-view { + -fx-background-insets: 0; + -fx-padding: 0; + -fx-background-color: derive(base, 20%); +} + +.list-cell { + -fx-background-color: transparent; + -fx-label-padding: 0 0 0 0; + -fx-graphic-text-gap : 0; + -fx-padding: 0 0 0 0; +} + +.list-cell:filled:selected #cardPane { + -fx-border-color: selected; + -fx-border-width: 2; +} + +.list-cell .label { + -fx-text-fill: text-default; +} + +.stack-pane { + -fx-background-color: accent; + -fx-text-fill: text-inverse; +} + +.result-display { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-font-family: "Inter Regular"; + -fx-font-size: 9pt; +} + +.result-display-label { + -fx-background-color: pane; + -fx-text-fill: text-faded !important; +} + +.grid-pane { + -fx-background-color: derive(base, 30%); + -fx-border-color: derive(base, 30%); + -fx-border-width: 1; +} + +.grid-pane .stack-pane { + -fx-background-color: derive(base, 30%); +} + +.menu .left-container { + -fx-background-color: black; +} + +.scroll-bar { + -fx-background-color: transparent; +} + +.scroll-bar .thumb { /* the bar itself */ + -fx-background-color: pane-light; + -fx-background-insets: 1; /* larger means thinner bar */ +} + +/* make other scroll controls invisible */ +.scroll-bar .increment-button, .scroll-bar .decrement-button, +.scroll-bar:horizontal .increment-arrow, .scroll-bar:horizontal .decrement-arrow { + -fx-opacity: 0; + -fx-padding: 0 0 0 0; +} + +.scroll-bar .increment-arrow, .scroll-bar .decrement-arrow { + -fx-shape: " "; +} + +.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow { + -fx-background-color: transparent; + -fx-opacity: 0; + -fx-padding: 1 3 1 3; +} + +#cardPane { + -fx-background-color: transparent; + -fx-border-width: 0; +} + +.cmd-txt-field { + -fx-background-color: transparent cmd-bg-and-box-outline transparent cmd-bg-and-box-outline; + -fx-background-insets: 0; + -fx-font-family: "Inter Regular"; + -fx-font-size: 13pt; + -fx-text-fill: text-inverse; + -fx-prompt-text-fill: text-faded; +} + +#filterField, #personListPanel, #personWebpage { + -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); +} + +#tags { + -fx-hgap: 8; + -fx-vgap: 8; +} + +#tags .label { + -fx-text-fill: accent; + -fx-background-color: tag; + -fx-padding: 7 7 7 7; + -fx-border-radius: 6; + -fx-background-radius: 2; + -fx-font-family: "Inter Regular"; + -fx-font-size: 11; +} + +#assigned { + -fx-hgap: 8; + -fx-vgap: 8; +} + +#assigned .label { + -fx-text-fill: tag-green; + -fx-background-color: tag-green-bg; + -fx-padding: 7 7 7 7; + -fx-background-radius: 6; + -fx-font-size: 11; +} + +.main-pane { + -fx-background-color: pane; + -fx-background-radius: 11; +} + +.border-pane { + -fx-background-color: bg; +} + +.person-card .label { + -fx-text-fill: text-inverse; + -fx-font-family: "Inter Regular"; + -fx-font-size: 12; +} + +.employees-list .label { + -fx-text-fill: text-inverse; + -fx-font-family: "Inter Medium"; + -fx-font-size: 12; +} + +.jobs-list .label { + -fx-text-fill: text-inverse; + -fx-font-family: "Inter Medium"; + -fx-font-size: 12; +} diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/styles/Extensions.css similarity index 65% rename from src/main/resources/view/Extensions.css rename to src/main/resources/styles/Extensions.css index bfe82a85964..641eeafb709 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/styles/Extensions.css @@ -1,6 +1,7 @@ - .error { - -fx-text-fill: #d06651 !important; /* The error class should always override the default text-fill style */ + /* changes the element to this color */ + /* Called by Ui.CommandBox.java, declared as ERROR_STYLE_CLASS */ + -fx-text-fill: #FF5736 !important; } .list-cell:empty { diff --git a/src/main/resources/styles/HelpPage.css b/src/main/resources/styles/HelpPage.css new file mode 100644 index 00000000000..5ced3a630e3 --- /dev/null +++ b/src/main/resources/styles/HelpPage.css @@ -0,0 +1,33 @@ +.root { + /* Final Colors */ + text-norm: white; + text-faded: #858AAB; + + bg: #1B1A2B; + pane: #201f31; + pane-light: #2e2d42; + accent: #6368ff; + + tag: #262645; + tag-green: #249b7e; + tag-green-bg: #202a38; +} + +.bottom-help-panel { + -fx-background-color: pane; + -fx-background-radius: 11px; +} + +.bottom-help-panel .label { + -fx-text-fill: text-norm; + -fx-font-family: "Inter Medium"; + -fx-font-size: 12; +} + +.button { + -fx-background-color: pane-light; + -fx-background-radius: 8; + -fx-text-fill: text-norm; + -fx-font-family: "Inter Medium"; + -fx-font-size: 12; +} diff --git a/src/main/resources/styles/PeoplesoftTable.css b/src/main/resources/styles/PeoplesoftTable.css new file mode 100644 index 00000000000..f54d61ff110 --- /dev/null +++ b/src/main/resources/styles/PeoplesoftTable.css @@ -0,0 +1,68 @@ +@import "DarkTheme.css"; + +.table-view { + -fx-base: base; + -fx-control-inner-background: base; + -fx-padding: 0; + -fx-background-color: transparent; + -fx-control-inner-background: transparent; + -fx-table-cell-border-color: transparent; + -fx-selection-bar-non-focused: transparent; +} + +.table-view .column-header-background { + -fx-background-color: #33344B; +} + +/* .table-view .column-header-background .filler { + -fx-background-color: #33344B; +} */ + +.table-view .column-header, .table-view .filler { + -fx-size: 50; + -fx-border-width: 0 0 0 0; + -fx-background-color: #33344B; + -fx-border-color: + #33344B + #33344B + #33344B + #33344B; + -fx-border-insets: 10 10 10 10; +} + +.table-view .column-header .label { + -fx-font-size: 13px; + -fx-font-family: "Inter"; + -fx-text-fill: text-norm; + -fx-font-weight: normal; + -fx-alignment: center-left; + -fx-opacity: 1; +} + +.table-view:focused .table-row-cell:filled:focused:selected { + -fx-background-color: transparent; + -fx-control-inner-background: transparent; + -fx-table-cell-border-color: transparent; +} + +.table-view .table-row-cell { + -fx-background-color: transparent; +} + +.table-view .table-cell { + -fx-text-fill: text-norm; + -fx-border-insets: 10 10 10 10; +} + +.table-view .table-cell .cell-text { + -fx-text-fill: text-norm; +} + +.table-view .table-cell .text { + -fx-fill: text-norm; +} + +.table-view .placeholder .label { + -fx-opacity: 0; + -fx-font-size: 0px; +} diff --git a/src/main/resources/styles/PeoplesoftTablePane.css b/src/main/resources/styles/PeoplesoftTablePane.css new file mode 100644 index 00000000000..3d38ec29843 --- /dev/null +++ b/src/main/resources/styles/PeoplesoftTablePane.css @@ -0,0 +1,17 @@ +@import "DarkTheme.css"; + +.header-label { + -fx-font-size: 20px; + -fx-font-family: "Inter"; + -fx-font-weight: bold; +} + +.pane { + -fx-background-color: pane; + -fx-background-radius: 11px; +} + +/* .table-view, .table-view .filler { + -fx-border-radius: 10 10 10 10; + -fx-background-radius: 10 10 10 10; +} */ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 09f6d6fe9e4..b0d51faa367 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -1,9 +1,27 @@ - - + + + + + - - + + + + + + + + + + + + + + + + + + - diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css deleted file mode 100644 index 36e6b001cd8..00000000000 --- a/src/main/resources/view/DarkTheme.css +++ /dev/null @@ -1,352 +0,0 @@ -.background { - -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ -} - -.label { - -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: #555555; - -fx-opacity: 0.9; -} - -.label-bright { - -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: white; - -fx-opacity: 1; -} - -.label-header { - -fx-font-size: 32pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-opacity: 1; -} - -.text-field { - -fx-font-size: 12pt; - -fx-font-family: "Segoe UI Semibold"; -} - -.tab-pane { - -fx-padding: 0 0 0 1; -} - -.tab-pane .tab-header-area { - -fx-padding: 0 0 0 0; - -fx-min-height: 0; - -fx-max-height: 0; -} - -.table-view { - -fx-base: #1d1d1d; - -fx-control-inner-background: #1d1d1d; - -fx-background-color: #1d1d1d; - -fx-table-cell-border-color: transparent; - -fx-table-header-border-color: transparent; - -fx-padding: 5; -} - -.table-view .column-header-background { - -fx-background-color: transparent; -} - -.table-view .column-header, .table-view .filler { - -fx-size: 35; - -fx-border-width: 0 0 1 0; - -fx-background-color: transparent; - -fx-border-color: - transparent - transparent - derive(-fx-base, 80%) - transparent; - -fx-border-insets: 0 10 1 0; -} - -.table-view .column-header .label { - -fx-font-size: 20pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-alignment: center-left; - -fx-opacity: 1; -} - -.table-view:focused .table-row-cell:filled:focused:selected { - -fx-background-color: -fx-focus-color; -} - -.split-pane:horizontal .split-pane-divider { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: transparent transparent transparent #4d4d4d; -} - -.split-pane { - -fx-border-radius: 1; - -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); -} - -.list-view { - -fx-background-insets: 0; - -fx-padding: 0; - -fx-background-color: derive(#1d1d1d, 20%); -} - -.list-cell { - -fx-label-padding: 0 0 0 0; - -fx-graphic-text-gap : 0; - -fx-padding: 0 0 0 0; -} - -.list-cell:filled:even { - -fx-background-color: #3c3e3f; -} - -.list-cell:filled:odd { - -fx-background-color: #515658; -} - -.list-cell:filled:selected { - -fx-background-color: #424d5f; -} - -.list-cell:filled:selected #cardPane { - -fx-border-color: #3e7b91; - -fx-border-width: 1; -} - -.list-cell .label { - -fx-text-fill: white; -} - -.cell_big_label { - -fx-font-family: "Segoe UI Semibold"; - -fx-font-size: 16px; - -fx-text-fill: #010504; -} - -.cell_small_label { - -fx-font-family: "Segoe UI"; - -fx-font-size: 13px; - -fx-text-fill: #010504; -} - -.stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); - -fx-border-top-width: 1px; -} - -.status-bar { - -fx-background-color: derive(#1d1d1d, 30%); -} - -.result-display { - -fx-background-color: transparent; - -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; - -fx-text-fill: white; -} - -.result-display .label { - -fx-text-fill: black !important; -} - -.status-bar .label { - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-padding: 4px; - -fx-pref-height: 30px; -} - -.status-bar-with-border { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 25%); - -fx-border-width: 1px; -} - -.status-bar-with-border .label { - -fx-text-fill: white; -} - -.grid-pane { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 30%); - -fx-border-width: 1px; -} - -.grid-pane .stack-pane { - -fx-background-color: derive(#1d1d1d, 30%); -} - -.context-menu { - -fx-background-color: derive(#1d1d1d, 50%); -} - -.context-menu .label { - -fx-text-fill: white; -} - -.menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.menu-bar .label { - -fx-font-size: 14pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-opacity: 0.9; -} - -.menu .left-container { - -fx-background-color: black; -} - -/* - * Metro style Push Button - * Author: Pedro Duque Vieira - * http://pixelduke.wordpress.com/2012/10/23/jmetro-windows-8-controls-on-java/ - */ -.button { - -fx-padding: 5 22 5 22; - -fx-border-color: #e2e2e2; - -fx-border-width: 2; - -fx-background-radius: 0; - -fx-background-color: #1d1d1d; - -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; - -fx-font-size: 11pt; - -fx-text-fill: #d8d8d8; - -fx-background-insets: 0 0 0 0, 0, 1, 2; -} - -.button:hover { - -fx-background-color: #3a3a3a; -} - -.button:pressed, .button:default:hover:pressed { - -fx-background-color: white; - -fx-text-fill: #1d1d1d; -} - -.button:focused { - -fx-border-color: white, white; - -fx-border-width: 1, 1; - -fx-border-style: solid, segments(1, 1); - -fx-border-radius: 0, 0; - -fx-border-insets: 1 1 1 1, 0; -} - -.button:disabled, .button:default:disabled { - -fx-opacity: 0.4; - -fx-background-color: #1d1d1d; - -fx-text-fill: white; -} - -.button:default { - -fx-background-color: -fx-focus-color; - -fx-text-fill: #ffffff; -} - -.button:default:hover { - -fx-background-color: derive(-fx-focus-color, 30%); -} - -.dialog-pane { - -fx-background-color: #1d1d1d; -} - -.dialog-pane > *.button-bar > *.container { - -fx-background-color: #1d1d1d; -} - -.dialog-pane > *.label.content { - -fx-font-size: 14px; - -fx-font-weight: bold; - -fx-text-fill: white; -} - -.dialog-pane:header *.header-panel { - -fx-background-color: derive(#1d1d1d, 25%); -} - -.dialog-pane:header *.header-panel *.label { - -fx-font-size: 18px; - -fx-font-style: italic; - -fx-fill: white; - -fx-text-fill: white; -} - -.scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); - -fx-background-insets: 3; -} - -.scroll-bar .increment-button, .scroll-bar .decrement-button { - -fx-background-color: transparent; - -fx-padding: 0 0 0 0; -} - -.scroll-bar .increment-arrow, .scroll-bar .decrement-arrow { - -fx-shape: " "; -} - -.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow { - -fx-padding: 1 8 1 8; -} - -.scroll-bar:horizontal .increment-arrow, .scroll-bar:horizontal .decrement-arrow { - -fx-padding: 8 1 8 1; -} - -#cardPane { - -fx-background-color: transparent; - -fx-border-width: 0; -} - -#commandTypeLabel { - -fx-font-size: 11px; - -fx-text-fill: #F70D1A; -} - -#commandTextField { - -fx-background-color: transparent #383838 transparent #383838; - -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; - -fx-border-insets: 0; - -fx-border-width: 1; - -fx-font-family: "Segoe UI Light"; - -fx-font-size: 13pt; - -fx-text-fill: white; -} - -#filterField, #personListPanel, #personWebpage { - -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); -} - -#resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; - -fx-background-radius: 0; -} - -#tags { - -fx-hgap: 7; - -fx-vgap: 3; -} - -#tags .label { - -fx-text-fill: white; - -fx-background-color: #3e7b91; - -fx-padding: 1 3 1 3; - -fx-border-radius: 2; - -fx-background-radius: 2; - -fx-font-size: 11; -} diff --git a/src/main/resources/view/HelpPage.fxml b/src/main/resources/view/HelpPage.fxml new file mode 100644 index 00000000000..f156875def0 --- /dev/null +++ b/src/main/resources/view/HelpPage.fxml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + +
diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css deleted file mode 100644 index 17e8a8722cd..00000000000 --- a/src/main/resources/view/HelpWindow.css +++ /dev/null @@ -1,19 +0,0 @@ -#copyButton, #helpMessage { - -fx-text-fill: white; -} - -#copyButton { - -fx-background-color: dimgray; -} - -#copyButton:hover { - -fx-background-color: gray; -} - -#copyButton:armed { - -fx-background-color: darkgray; -} - -#helpMessageContainer { - -fx-background-color: derive(#1d1d1d, 20%); -} diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml deleted file mode 100644 index 5dea0adef70..00000000000 --- a/src/main/resources/view/HelpWindow.fxml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/view/JobListCard.fxml b/src/main/resources/view/JobListCard.fxml new file mode 100644 index 00000000000..f0722ab6434 --- /dev/null +++ b/src/main/resources/view/JobListCard.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/view/JobListPanel.fxml b/src/main/resources/view/JobListPanel.fxml new file mode 100644 index 00000000000..8836dd4d4ec --- /dev/null +++ b/src/main/resources/view/JobListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index a431648f6c0..3557327b516 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -1,60 +1,61 @@ - - - - - - - - - - + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+
+
+
+
diff --git a/src/main/resources/view/OverviewPage.fxml b/src/main/resources/view/OverviewPage.fxml new file mode 100644 index 00000000000..74bef907345 --- /dev/null +++ b/src/main/resources/view/OverviewPage.fxml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PeoplesoftTablePane.fxml b/src/main/resources/view/PeoplesoftTablePane.fxml new file mode 100644 index 00000000000..0bda3a4c63e --- /dev/null +++ b/src/main/resources/view/PeoplesoftTablePane.fxml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f08ea32ad55..afe709bfe4b 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -1,36 +1,21 @@ - - - - - - - - + + + - - - - - - - - - - - - - - - + + + + + + diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml index 8836d323cc5..a917968e26b 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/PersonListPanel.fxml @@ -1,8 +1,8 @@ - - + + - - + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 58d5ad3dc56..96c19691d36 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -1,9 +1,20 @@ - - + + + - -