diff --git a/.gitignore b/.gitignore index 823d175eb670..e686ad968d87 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ lib/* *.log.* *.csv config.json +!_reposense/config.json src/test/data/sandbox/ preferences.json .DS_Store diff --git a/README.adoc b/README.adoc index 450054624f48..1ab336a09eb9 100644 --- a/README.adoc +++ b/README.adoc @@ -1,10 +1,12 @@ -= Address Book (Level 4) += Heart² ifdef::env-github,env-browser[:relfileprefix: docs/] -https://travis-ci.org/se-edu/addressbook-level4[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]] -https://ci.appveyor.com/project/damithc/addressbook-level4[image:https://ci.appveyor.com/api/projects/status/3boko2x2vr5cc3w2?svg=true[Build status]] -https://coveralls.io/github/se-edu/addressbook-level4?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master[Coverage Status]] -https://www.codacy.com/app/damith/addressbook-level4?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level4&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]] +_Enterprise Wedding Management System_ + +https://travis-ci.org/CS2103-AY1819S1-F10-3/main[image:https://travis-ci.org/CS2103-AY1819S1-F10-3/main.svg?branch=master[Build Status]] +https://ci.appveyor.com/project/liaujianjie/main[image:https://ci.appveyor.com/api/projects/status/ycx5vnhfck9tp9ae?svg=true[Build status]] +https://coveralls.io/github/CS2103-AY1819S1-F10-3/main?branch=master[image:https://coveralls.io/repos/github/CS2103-AY1819S1-F10-3/main/badge.svg?branch=master[Coverage Status]] +https://www.codacy.com/app/liaujianjie/main?utm_source=github.com&utm_medium=referral&utm_content=CS2103-AY1819S1-F10-3/main&utm_campaign=Badge_Grade"[image:https://api.codacy.com/project/badge/Grade/cd2ccc2fc61c4afdac9c3f89a3345a65[Codacy Badge]] https://gitter.im/se-edu/Lobby[image:https://badges.gitter.im/se-edu/Lobby.svg[Gitter chat]] ifdef::env-github[] @@ -15,26 +17,20 @@ ifndef::env-github[] image::images/Ui.png[width="600"] endif::[] -* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as the main programming language. -* It is *written in OOP fashion*. It provides a *reasonably well-written* code example that is *significantly bigger* (around 6 KLoC)than what students usually write in beginner-level SE modules. -* What's different from https://github.com/se-edu/addressbook-level3[level 3]: -** A more sophisticated GUI that includes a list panel and an in-built Browser. -** More test cases, including automated GUI testing. -** Support for _Build Automation_ using Gradle and for _Continuous Integration_ using Travis CI. +* This is a desktop Wedding Planner application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). +* This Wedding Planner application is intended for Wedding Planner companies. +* It matches clients with their service providers based on various client requests. == Site Map * <> * <> -* <> * <> * <> == Acknowledgements -* Some parts of this sample application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by -_Marco Jakob_. +* This application was adapted from a SE-EDU initiative: https://github.com/se-edu/[AddressBook-Level4] * Libraries used: https://github.com/TestFX/TestFX[TextFX], https://bitbucket.org/controlsfx/controlsfx/[ControlsFX], https://github.com/FasterXML/jackson[Jackson], https://github.com/google/guava[Guava], https://github.com/junit-team/junit5[JUnit5] == Licence : link:LICENSE[MIT] diff --git a/_reposense/config.json b/_reposense/config.json new file mode 100644 index 000000000000..70c996aeb218 --- /dev/null +++ b/_reposense/config.json @@ -0,0 +1,30 @@ +{ + "authors": + [ + { + "githubId": "dongsiji", + "displayName": "DON...IJI", + "authorNames": ["dongsiji"] + }, + { + "githubId": "NightYeti", + "displayName": "GAN C...N YAO", + "authorNames": ["NightYeti", "Gan Chin Yao"] + }, + { + "githubId": "liaujianjie", + "displayName": "LIAU ...N JIE", + "authorNames": ["liaujianjie"] + }, + { + "githubId": "wailunlim", + "displayName": "LIM W...I LUN", + "authorNames": ["wailunlim"] + }, + { + "githubId": "eehooi", + "displayName": "NG EE... HOOI", + "authorNames": ["eehooi"] + } + ] +} diff --git a/build.gradle b/build.gradle index f8e614f8b49b..a7b9b9a32a14 100644 --- a/build.gradle +++ b/build.gradle @@ -207,8 +207,8 @@ asciidoctor { idprefix: '', // for compatibility with GitHub preview idseparator: '-', 'site-root': "${sourceDir}", // must be the same as sourceDir, do not modify - 'site-name': 'AddressBook-Level4', - 'site-githuburl': 'https://github.com/se-edu/addressbook-level4', + 'site-name': 'main', + 'site-githuburl': 'https://github.com/CS2103-AY1819S1-F10-3/main', 'site-seedu': true, // delete this line if your project is not a fork (not a SE-EDU project) ] diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index e647ed1e715a..45e60815a651 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -4,53 +4,53 @@ :imagesDir: images :stylesDir: stylesheets -AddressBook - Level 4 was developed by the https://se-edu.github.io/docs/Team.html[se-edu] team. + -_{The dummy content given below serves as a placeholder to be used by future forks of the project.}_ + +Heart² was developed by the F10-3 team of NUS School of Computing AY18/19. + {empty} + We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. == Project Team -=== John Doe -image::damithc.jpg[width="150", align="left"] -{empty}[http://www.comp.nus.edu.sg/~damithch[homepage]] [https://github.com/damithc[github]] [<>] +=== Dong SiJi +image::dongsiji.png[width="150", align="left"] +{empty}[https://github.com/dongsiji[github]] [<>] -Role: Project Advisor +Role: Testing, Java guru + +Components: Model, Logic ''' -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] +=== Gan Chin Yao +image::nightyeti.png[width="150", align="left"] +{empty}[http://github.com/NightYeti[github]] [<>] -Role: Team Lead + -Responsibilities: UI +Role: Team Lead, Deliverables + +Components: Logic, Storage ''' -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] +=== Liau Jian Jie +image::liaujianjie.png[width="150", align="left"] +{empty}[http://github.com/liaujianjie[github]] [<>] -Role: Developer + -Responsibilities: Data +Role: Code quality, Git expert + +Components: Logic, Storage ''' -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Lim Wai Lun +image::wailunlim.png[width="150", align="left"] +{empty}[http://github.com/wailunlim[github]] [<>] -Role: Developer + -Responsibilities: Dev Ops + Threading +Role: Integration, Java guru + +Components: Model, Logic ''' -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] +=== Ng Ee Hooi +image::eehooi.png[width="150", align="left"] +{empty}[http://github.com/eehooi[github]] [<>] -Role: Developer + -Responsibilities: UI +Role: Documentation, UI master + +Components: Logic, UI ''' diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc index 5de5363abffd..add19a6af1d4 100644 --- a/docs/ContactUs.adoc +++ b/docs/ContactUs.adoc @@ -2,6 +2,6 @@ :site-section: ContactUs :stylesDir: stylesheets -* *Bug reports, Suggestions* : Post in our https://github.com/se-edu/addressbook-level4/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. +* *Bug reports, Suggestions* : Post in our https://github.com/CS2103-AY1819S1-F10-3/main/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. * *Contributing* : We welcome pull requests. Follow the process described https://github.com/oss-generic/process[here] -* *Email us* : You can also reach us at `damith [at] comp.nus.edu.sg` +* *Email us* : You can reach out to our Team Lead at `gan@u.nus.edu` diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 817ec81d7832..9f523de104d7 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,8 +1,8 @@ -= AddressBook Level 4 - Developer Guide += Heart² - Developer Guide :site-section: DeveloperGuide :toc: -:toc-title: -:toc-placement: preamble +:toc-title: What's in this Developer Guide: +:toc-placement: macro :sectnums: :imagesDir: images :stylesDir: stylesheets @@ -13,11 +13,49 @@ ifdef::env-github[] :warning-caption: :warning: :experimental: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master +:repoURL: https://github.com/CS2103-AY1819S1-F10-3/main/tree/master -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +image::guidefordevelopers.png[width="600"] -== Setting up +By: `Team Heart²` Since: `Aug 2018` Last updated: `November 2018` Licence: `MIT` + +== Introduction + +Welcome to *_Heart²_*! *_Heart²_* is a desktop software intended to make the job of wedding planning agencies simpler. +It provides simple yet powerful features to efficiently manage clients' and agency companies' profiles. +Users can find suitable vendors providing wedding services for couples using just a few keystrokes with our enterprise feature set. + +This developer guide is a self-contained resource designed to align all developers around a common vision. It helps +developers of all levels learn more about the workings behind the scenes and how to make use of them effectively. + +So if you want to know how to make *_Heart²_* even better, here's where you start! + + + +image::calloutpic.png[width="256"] + +Callouts are rectangular boxes with an icon and words to point out some information. Below are 3 callouts that will be used throughout this document: + +[NOTE] +This represents a *note*. A note indicates additional important information. Be sure to read these notes as they might be applicable to you! + +[TIP] +This represents a *tip*. A tip denotes something that is often handy, and good for you to know. Tips are often less crucial, and you can choose to skip them. + +[WARNING] +This represents a *warning*. A warning denotes something of crucial importance, and you should be extremely cautious when reading the statement. + +{empty} + + +toc::[] + +== Setting Up + +image::settingup.png[width="200"] + +This section provides information on setting up your local computer and importing all the necessary tools required to run this application. + +[WARNING] +Read this section in detail and follow the configurations carefully. Otherwise, the application may not work as expected. === Prerequisites @@ -105,12 +143,19 @@ When you are ready to start coding, == Design +image::designheader.png[width="320"] + +This section shows an overview of the design decisions for this application. It serves to allow you to better understand the various components linking this application together. + + [[Design-Architecture]] === Architecture -.Architecture Diagram +._Architecture Diagram_ image::Architecture.png[width="600"] +{empty} + + The *_Architecture Diagram_* given above explains the high-level design of the App. Given below is a quick overview of each component. [TIP] @@ -140,24 +185,26 @@ Each of the four components For example, the `Logic` component (see the class diagram given below) defines it's API in the `Logic.java` interface and exposes its functionality using the `LogicManager.java` class. -.Class Diagram of the Logic Component +._Class Diagram of the Logic Component_ image::LogicClassDiagram.png[width="800"] +{empty} + + [discrete] ==== Events-Driven nature of the design -The _Sequence Diagram_ below shows how the components interact for the scenario where the user issues the command `delete 1`. +The _Sequence Diagram_ below shows how the components interact for the scenario where the user issues the command `client#1 delete`. -.Component interactions for `delete 1` command (part 1) -image::SDforDeletePerson.png[width="800"] +._Component interactions for `client#1 delete ` command (part 1)_ + +image::SdForDeleteClient.png[width="800"] [NOTE] Note how the `Model` simply raises a `AddressBookChangedEvent` when the Address Book data are changed, instead of asking the `Storage` to save the updates to the hard disk. The diagram below shows how the `EventsCenter` reacts to that event, which eventually results in the updates being saved to the hard disk and the status bar of the UI being updated to reflect the 'Last Updated' time. -.Component interactions for `delete 1` command (part 2) -image::SDforDeletePersonEventHandling.png[width="800"] +._Component interactions for `client#1 delete` command (part 2)_ + +image::SdForDeleteClientEventHandling.png[width="800"] [NOTE] Note how the event is propagated through the `EventsCenter` to the `Storage` and `UI` without `Model` having to be coupled to either of them. This is an example of how this Event Driven approach helps us reduce direct coupling between components. @@ -167,12 +214,14 @@ The sections below give more details of each component. [[Design-Ui]] === UI component -.Structure of the UI Component +._Structure of the UI Component_ image::UiClassDiagram.png[width="800"] +{empty} + + *API* : link:{repoURL}/src/main/java/seedu/address/ui/Ui.java[`Ui.java`] -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter`, `BrowserPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class. +The UI consists of a `MainWindow`. The `MainWindow` is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter`, `BrowserPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class. The `UI` component uses 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 link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] @@ -186,9 +235,11 @@ The `UI` component, === Logic component [[fig-LogicClassDiagram]] -.Structure of the Logic Component +._Structure of the Logic Component_ image::LogicClassDiagram.png[width="800"] +{empty} + + *API* : link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] @@ -197,23 +248,28 @@ link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] . The command execution can affect the `Model` (e.g. adding a person) and/or raise events. . The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. -Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` API call. +Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("client#1 delete")` API call. + +._Interactions Inside the Logic Component for the `client#1 delete` Command_ +image::DeleteClientSdForLogic.png[width="800"] -.Interactions Inside the Logic Component for the `delete 1` Command -image::DeletePersonSdForLogic.png[width="800"] +{empty} + [[Design-Model]] === Model component -.Structure of the Model Component +._Structure of the Model Component_ image::ModelClassDiagram.png[width="800"] +{empty} + + *API* : link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model.java`] The `Model`, * stores a `UserPref` object that represents the user's preferences. * stores the Address Book data. +* stores the Account data that was used to log in. * exposes 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. * does not depend on any of the other three components. @@ -225,9 +281,11 @@ image:ModelClassBetterOopDiagram.png[width="800"] [[Design-Storage]] === Storage component -.Structure of the Storage Component +._Structure of the Storage Component_ image::StorageClassDiagram.png[width="800"] +{empty} + + *API* : link:{repoURL}/src/main/java/seedu/address/storage/Storage.java[`Storage.java`] The `Storage` component, @@ -235,6 +293,21 @@ The `Storage` component, * can save `UserPref` objects in json format and read it back. * can save the Address Book data in xml format and read it back. +=== Account Storage component + +._Structure of the Account Storage Component_ +image::AccountStorageClassDiagram.png[width="800"] + +{empty} + + +*API* : link:{repoURL}/src/main/java/seedu/address/storage/AccountStorage.java[`AccountStorage.java`] + +The `AccountStorage` component + +* can save the Account data in xml format and read it back. +* can populate a default root Account data in xml format if missing. +* can update existing Account password stored in the storage. + [[Design-Commons]] === Common classes @@ -242,6 +315,9 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa == Implementation +image::implementationheader.png[width="400"] + +Before you start, you'd need to find out how *_Heart²_*'s features work! This section describes some noteworthy details on how certain features are implemented. // tag::undoredo[] @@ -322,14 +398,21 @@ image::UndoRedoActivityDiagram.png[width="650"] * **Alternative 2:** Use `HistoryManager` for undo/redo ** Pros: We do not need to maintain a separate list, and just reuse what is already in the codebase. ** Cons: Requires dealing with commands that have already been undone: We must remember to skip these commands. Violates Single Responsibility Principle and Separation of Concerns as `HistoryManager` now needs to do two different things. -// end::undoredo[] - -// tag::dataencryption[] -=== [Proposed] Data Encryption -_{Explain here how the data encryption feature will be implemented}_ +// tag::undoredoDisplay[] +===== Aspect: What it shows after undo/redo command successfully executes -// end::dataencryption[] +* *Alternative 1 (current choice):* Shows the list that was changed due to the undo/redo command. +** Pros: Easy for the user to identify what was changed, whether a client or vendor was modified. +** Cons: It switches the list out of the current filter and the user have to re-type the list command if he wants to filter the list. +* *Alternative 2:* Keeps showing what was shown before the command was executed. +** Pros: Easy to implement. +** Cons: Hard for the user to identify what was changed in the addressbook. +* *Alternative 3:* Show what was changed, before and after. +** Pros: User can easily tell what was changed. +** Cons: Hard to implement, need to have an additional UI components to show what was changed and need additional components to store the list before it was changed. +// end::undoredoDisplay[] +// end::undoredo[] === Logging @@ -351,7 +434,553 @@ We are using `java.util.logging` package for logging. The `LogsCenter` class is Certain properties of the application can be controlled (e.g App name, logging level) through the configuration file (default: `config.json`). +// tag::login[] +=== Login Feature + +Before the user can use *_Heart²_*, they must first log in with a registered account. + +==== Before logging in +The user is presented with a login UI: + +._The login screen when user launches the application._ +image::UiLoginDiagram.png[width="800"] + +{empty} + + +There are only 3 commands available for user to execute: + +* `login` : Login to the system with a username and password +* `help` : Show the help panel +* `exit` : Quit the application + +==== After logging in +The `LoginWindow` is hidden and the `MainWindow` is shown upon successful login. + +The user can execute all available commands, if the user-account is given the correct privilege. However, the user cannot execute the `login` command again since he is already logged in. + +// tag::loginui[] +==== Design Considerations +===== Aspect: How to show the Login UI +* *Alternative 1 (current choice):* Deploy the `LoginWindow` upon launch by parsing a new `loginStage` each time. +** Pros: Similar to existing applications, easier for users to use. Hides all `MainWindow` details. +** Cons: Creation of a new `loginStage` may result in performance issues. +* *Alternative 2:* Deploy the `LoginWindow` as a modal window above the `MainWindow`. +** Pros: Easy to implement. +** Cons: Users are able to see the `MainWindow` before login. Requires the hiding and showing of inner parts in the `MainWindow` which may result in performance issues. +// end::loginui[] + +=== Account creation +An account is created for the purpose of logging in and authenticating the user, before the user is allowed to use the application. This protects the confidentiality and data integrity of the application. + +[NOTE] +The user can only register for an account via an existing account with `SUPER_USER` privilege. It may sound counter-intuitive to require an account before registering a new account. We make this requirement as only authorised personal should be given an account. Ideally, the owner of the application should dictate the account given to employees by helping them register an account. + +==== Types of account +There are 2 types of accounts: + +* `SUPER_USER` : A user that is capable of executing all commands available in the application. +* `READ_ONLY_USER` : A user that is capable of executing all commands except registering new account, adding, editing, and deleting entries in the database. + +These 2 types of accounts are referred as Role and facilitated by the `Role` enum. + +The restrictions of a `READ_ONLY_USER` is enforced by the methods found in `Account` class, specifically: + +* `boolean hasWritePrivilege()` +* `boolean hasDeletePrivilege()` +* `boolean hasAccountCreationPrivilege()` + +Commands that prevents a `READ_ONLY_USER` from executing is checked with a condition as such: + +[source,java] +if (!account.hasWritePrivilege()) { + throw new LackOfPrivilegeException(COMMAND_WORD); +} + +==== Design Considerations +===== Aspect: Should the privilege be tied to Role enum or Account class? +* *Alternative 1 (current choice):* Account class contains the privileges methods such has `hasWritePrivilege`. +** Pros: This makes sense as the type of privilege is tied to the account. +* *Alternative 2:* Role enum should contains the privileges methods +** Pros: Since Role enum contains all the different roles such as `READ_ONLY_USER` and `SUPER_USER`, it is easy to reference all the different types of roles and the privileges in 1 file. This makes adding more roles and privileges in the future easy. +** Cons: It sounds awkward to have privileges associated with Role rather than with an Account. + +===== Aspect: What type of access control to use? +* *Alternative 1 (current choice):* Role based access control. (RBAC) +** Pros: Most relevant in the context of this application. Allows application owner to set privileges for employees. +** Cons: User does not have a say in access control, even in content created by them. +* *Alternative 2:* Discretionary Access Control (DAC) +** Pros: Less restrictive. Allows individual complete control over content they have created. +** Cons: Not really applicable in our context as we want to restrict employee access to data. Employee's access control based on their individual roles in the company seems more appropriate than employees having access based on the content they create. + + +=== Account storage +All accounts are stored in a file call `/data/accountlist.xml`. This file is generated on the fly during first launch and populated with a root account. By default, a root account is hardcoded into the application with the username `rootUser` and password `rootPassword` with the role `SUPER_USER`. + +The diagram below shows what happens when a user launches the application: + +._Activity diagram when user launches the application_ +image::accountstoragediagram.png[width="800"] + +{empty} + + +Only a `SUPER_USER` is allowed to create a new account, either for himself, or on behalf of another person. The diagram below shows what happen when a user attempts to register a new account: + +._Activity diagram when user registers an account_ +image::accountcreationdiagram.png[width="800"] + +{empty} + + +To change the default root account, navigate to the static method `getRootAccount()` in `Account.java`: + +[source,java] +public static Account getRootAccount() { + return new Account("rootUser", "rootPassword", Role.SUPER_USER); +} + +Change the `rootUser`, `rootPassword`, and `Role.SUPER_USER` accordingly to your needs. It is necessary to have a default root account, otherwise no account will exist and one cannot execute the `register account` command without first logging in. + +[WARNING] +It is advisable to assign `SUPER_USER` as the root account. Otherwise, you are not able to register any other account without first logging in to a `SUPER_USER` account. + +==== Design Considerations +===== Aspect: What file type to store user account as? +* *Alternative 1 (current choice):* Store it as a `xml` file locally. +** Pros: The code to write and read xml file is already present for adding address book contact initially in the Address Book - level 4 app. Hence, adopting this code and modifying it for account storage is easier than coming up with code from scratch. +** Cons: Relatively wordy and verbose with all the opening and closing tag. For the same amount of account information, compared to other format such as `json`, more data has to be stored to account for tag elements. +* *Alternative 2:* Store it as a `json` file locally. +** Pros: Simpler syntax than `xml` and hence less data is required to store the same amount of account information. +** Pros: Can be parsed into a ready-to-use JavaScript object. +** Cons: Not familiar with json, hence more effort is needed to write code to store account in json format, compared to the already given code for xml storage. + +==== Security Considerations + +===== Database +Currently, the list of accounts is stored locally on data/accountlist.xml. For security purposes, we may consider the following implementations in the future for v2.0: + +* **Encrypt accountlist.xml:** This can prevent direct lookup of the file as the content is encrypted +* **Store the file on a server:** Due to project restriction, we are unable to implement this at v1.4. Storing file on a server has an added advantage of utilising web security practises or employing third party services to help protect our account list in private servers. + +===== Storing password +Username is stored in plaintext in accountlist.xml, as username is not private information. However, user password is hashed with `PBKDF2WithHmacSHA512` algorithm together with a `salt`, to prevent password from being visible in plaintext. `PBKDF2WithHmacSHA512` is deliberately chosen as it is a link:https://adambard.com/blog/3-wrong-ways-to-store-a-password/[slower] algorithm, thus slowing down brute-force attack for finding out user password. The hashing algorithm is present in `PasswordAuthentication` class and the implementation is based off this link:http://stackoverflow.com/a/2861125/3474[stackoverflow] answer. + +=== Unique ID feature +*_Heart²_* assigns a unique ID to every `client` and `vendor` when they are added into *_Heart²_*. +This ID is unique within their contact type, meaning that a `client` and a `vendor` may have the same ID, but since this ID comes hand in hand with the contact type, they are effectively unique. +These IDs are last for a single session, and *_Heart²_* reassigns the IDs at the start of the next session. + +==== Current Implementation +Both the `Client` and `Vendor` class have a `public static` running counter starting from 1. +When a `client` or `vendor` is created, it is assigned that number, before incrementing it by 1. +The `contact` then has this ID for this session, and the user can use this ID, coupled with the contact type to always refer to this particular contact. + +This unique ID is used by many other commands, namely: `add`, `delete`, `update`, `view`, `addservice`, `automatch`. +It allows for these commands to be executed at any point in *_Heart²_*, with always the same context. + +==== Design Considerations +===== Aspect: How should we refer to contacts in *_Heart²_*? + +* *Alternative 1*: +Use the legacy implementation, which is to use the relative position of the contact in the list. + +** Pros: No change is required, as it is the legacy implementation. + +** Cons: Users have to navigate to a list that shows that contact, and the relative position of that contact may keep changing throughout a session. + +* *Alternative 2* (current choice): + +** Pros: Users are able to refer back to a particular contact at any time, without requiring the current list shown to contain that contact. +Also, this ID will never change during a session, so the user can confidently use the ID knowing that it will always refer to that contact. + +** Cons: Users still have to remember this unique ID to refer back to the contact. It might be hard to remember the ID. + +After much consideration, we decided to go with option 2. +*_Heart²_* is built for speed, and we would like to give our users flexibility to execute any command within *_Heart²_* at any time. +We believe that this can give users more control and power over their work using *_Heart²_*, and therefore we chose to implement this unique ID system. + +However, we also do realise that users might find it hard to remember the unique ID assigned to the contact. +While users can quickly look at a recent contact using the command `history`, a possibly quality-of-life improvement would be to implement a mnemonic unique identifier. + +=== Add contact feature +*_Heart²_* requires users to explicitly specify whether the contact to be added is a `client` or a `vendor` in the command. + +* `client add n/Wai Lun p/90463327 e/wailun@u.nus.edu a/PGP House` +* `vendor add n/Lun Wai p/72336409 e/lunwai@u.nus.edu a/RVRC` + +The above commands add a `client` and a `vendor`, together with the details provided, respectively. + +This differentiation between `client` and `vendor` facilitates many other features of *_Heart²_*. +It complements the unique ID feature earlier to ensure that a `client` and a `vendor` with the same ID are still differentiable due to the contact type. + +Adding of duplicate contacts are not allowed in *_Heart²_*. +[NOTE] +A contact is considered a duplicate if they are of the same contact type *and* have the same name *and* have *either* the same phone number *or* email address. + +==== Current Implementation +Both `Client` and `Vendor` classes inherit from an abstract `Contact` class. +When adding a contact, either a new `Client` or a `Vendor` object is instantiated. +Both `Client` and `Vendor` objects are added to a list of generic type `Contact`. + +In order to differentiate them, there is an abstract method `Contact#getType()` that `Client` and `Vendor` implement differently. +`Client` objects return a `ContactType.CLIENT` enum while `Vendor` objects return a `ContactType.VENDOR` enum. + +When adding a contact, the parser first distinguishes whether it is an addition of a `client` or `vendor`. +The correct `ContactType` enum is then passed to `AddCommandParser`. +The `AddCommandParser` parses the argument to create the appropriate contact, and creates the appropriate `AddCommand`. + +This `AddCommand` is passed back to the `LogicManager`, and the method `execute()` is called. +The contact is then added to the model. + +Below is the sequence diagram of a `client add` command. + +image::AddClientSequenceDiagram.png[width="800"] + + + +==== Design Considerations +===== Aspect: How should we store `Client` and `Vendor` objects in *_Heart²_*? + +* *Alternative 1* (current choice): +`Client` and `Vendor` objects are stored in a more general `Contact` list. +** Pros: Easy to implement by tweak the inherited legacy list slightly. + +** Cons: Cannot tell immediately if an element in the `Contact` list is a `Client` or `Vendor`. +This might take a longer time to display lists, due to having to filter them every time. + +* *Alternative 2*: +Hold `Client` and `Vendor` objects differently in two different lists. +** Pros: Able to get `Client` or `Vendor` immediately without having to go through the entire `Contact` list as in alternative 1. +** Cons: Difficult and extremely tedious to implement. + +===== Aspect: How restrictive should the definition of a duplicate contact be? + +* *Alternative 1*: +It should be regardless of contact type, meaning a `client` and a `vendor` cannot have the same name *and* either the same phone number *or* email. + +** Pros: No additional implementation required. The legacy implementation already supports this. + +** Cons: Less flexibility for our users. A `client` cannot be a `vendor` possibly. + +* *Alternative 2* (current choice): +A `client` and a `vendor` can have similar fields, meaining a `client` and a `vendor` can possibly have the same name, phone number *and/or* email. + +** Pros: More flexibility for our users. A `client` can be a `vendor` too, which is possible in the real world. + +** Cons: Additional implementation to have. + +=== Delete contact feature +*_Heart²_* allows the user to delete any contact, using the combination of its contact type and its unique ID, followed by `delete`. + +* `client#2 delete` +* `vendor#3 delete` + +The above commands delete the `client` given the unique ID #2 and the `vendor` given the unique ID #3. + +This feature makes use of the fact that contacts are either `Client` or `Vendor` objects. +The unique ID is then used to identify the particular `Client` or `Vendor` object to be deleted. + +==== Current Implementation +The current implementation filters the contact list by the specified contact type and ID. +The predicate to filter by `client` and `vendor` can be retrieved by `ContactType.CLIENT#getFilter()` and `ContactType.VENDOR#getFilter()` respectively. +This first predicate is then combined with another predicate that is looking for the ID of the `client` or `vendor`, specified in the command. + +Since IDs are unique, after filtering by this (combined) predicate, the list can only have a maximum length of 1. +This would indicate that the contact in the list is the contact we are to delete. +However, if the list is of size 0, this means that the contact that is specified to be deleted does not exist. +We then can feedback to the user that the ID specified is invalid. + +[TIP] +The sequence diagram for the `delete` command can be found together with the structure of the logic component <>. + +=== Update contact feature +*_Heart²_* allows the user to update any contact, using the combination of its contact type and its unique ID, followed by `update`. + +* 'client#1 update n/Wai Lua' +* 'vendor#2 update p/9046 3328' + +The above commands update the name of the `client` given the unique ID #2 to "Wai Lua", and the phone number of the `vendor` given the unique ID #2 to 9046 3328. + +[NOTE] +A contact is considered a duplicate if they are of the same contact type *and* have the same name *and* have *either* the same phone number *or* email address. + +As mentioned earlier, duplicate contacts are not allowed in *_Heart²_*. +Thus, when updating, the user is not allowed to update a contact in *_Heart²_* if updating it will result in duplicated contacts. + +==== Implementation + +The current implementation uses part of the legacy implementation to do the updating of contacts. +The arguments are parsed and tokenized using `ArgumentTokenizer#tokenize(String, Prefix...)`. +An `EditContactDescriptor` object is then created to hold this new information temporarily. + +[TIP] +The prefixes applicable to `update` are `n/`, `p/`, `e/`, `a/`, `t/`. At least one of them must follow the `update` command. + +Then, the current implementation filters the contact list by the specified contact type and ID. +The predicate to filter by `client` and `vendor` can be retrieved by `ContactType.CLIENT#getFilter()` and `ContactType.VENDOR#getFilter()` respectively. +This first predicate is then combined with another predicate that is looking for the ID of the `client` or `vendor`, specified in the command. + +Since IDs are unique, after filtering by this (combined) predicate, the list can only have a maximum length of 1. +The contact in this list would be the contact to be updated. +A new contact is created using `UpdateCommand#createEditedContact(Contact, EditContactDescriptor, ContactType)`. +The next step is then to ensure that this new contact is not a duplicate contact, before replacing the old contact in *_Heart²_*. + +[NOTE] +A contact is considered a duplicate if they are of the same contact type *and* have the same name *and* have *either* the same phone number *or* email address. + +However, if the list is of size 0, this means that the contact that is specified to be deleted does not exist. +We then can feedback to the user that the ID specified is invalid. + +==== Design Considerations +===== Aspect: Should a similar (have the same name *and* same phone number *or* email) `client` and `vendor` object be updated together? + +* *Alternative 1* (current choice): +A `client` can be similar to a `vendor`, but they are still considered independent contacts in *_Heart²_*. + +** Pros: No implementation required. The user has the choice to have their `client` and `vendor` be similar or not be. + +** Cons: There are scenarios where the user would like to update the details of both the `client` and `vendor` together as they are similar. +In this case, they will have to update both manually. + +* *Alternative 2*: +Updating a similar `client` or `vendor` should update its counterpart. + +** Pros: The user can update a similar `client` and `vendor` together. + +** Cons: Hard to implement. Also, the user will lack the flexibility of having his `client` and `vendor` be updated individually. + +After much consideration, we decided to choose option 1, so that our user can have more flexibility in *_Heart²_*. +A lot of people have separate phone numbers and emails for personal use and work, and thus it made sense to us that these contacts should still be updated separately. + +However, there will definitely be cases where users might want such similar contacts to be linked and updated together. +A possible quality-of-life improvement would be to allow an option (command) to link such similar contacts to each other, for updating. + +=== View contact feature +*_Heart²_* allows the user to view any contact, using the combination of its contact type and its unique ID, followed by `view`. + +* `client#3 view` +* `vendor#6 view` + +The above commands selects the `client` with the unique ID #3 and the `vendor` with the unique ID #6 respectively for viewing. +The contact's card is shown on the panel on the right in *_Heart²_*, containing all the information regarding the contact. + +[TIP] +By using `view`, all the information regarding the contact will be shown. +This includes the name, phone number, email address, tags, residential (`client`) or office address (`vendor`), and services requested (`client`) or services offered (`vendor`). + +==== Implementation +The `view` command uses a similar implementation to the `add`, `delete` and `update` commands. +The current implementation filters the contact list by the specified contact type and ID. +The predicate to filter by `client` and `vendor` can be retrieved by `ContactType.CLIENT#getFilter()` and `ContactType.VENDOR#getFilter()` respectively. +This first predicate is then combined with another predicate that is looking for the ID of the `client` or `vendor`, specified in the command. + +Since IDs are unique, after filtering by this (combined) predicate, the list can only have a maximum length of 1. +After obtaining this contact, a new `JumpToListRequestEvent` is posted to the event bus. +The `PersonListPanel` class is registered as an event handler, and it handles this `JumpToListRequestEvent` with `PersonListPanel#handleJumpToListRequestEvent(JumpToListRequestEvent)`. +The contact's card is then selected for viewing. + +However, if the list is of size 0, this means that the contact that is specified for viewing does not exist. +We then can feedback to the user that the ID specified is invalid. + +==== Design Considerations +The implementation of `view` was chosen to also use the combination of the contact type and ID to select a contact for viewing. +This is part of a standardisation effort to have cohesiveness in the command syntax. + +// tag::addservice[] +=== Adding a service request feature +*_Heart²_* allows the user to add attributes of the services the user's clients require or vendors can provide. + They can be indicated using the `addservice` command, by their unique IDs: + + * `client#123 addservice s/photographer c/1000.00` + * `vendor#123 addservice s/photographer c/1000.00` + +==== Implementation + +Given below is a sequence diagram of how the `addservice` operation works: + +.Add Service Command Sequence Diagram +image::AddServiceSequenceDiagram.png[width="800] + +The command is first parsed into the `AddServiceCommandParser`, which breaks up the arguments into their respective fields. +A new `Service` is created and parsed into the `AddServiceCommand` along with the ID. + +Only when there is no existing request of that service type that the specified service request can be added with `AddServiceCommand#createContactWithService()`. +All fields of the `Contact` including the existing services would be copied over to a new `Contact` with the `EditContactDescriptor` in `UpdateCommand`. +Next, the new service would be added with `EditCommandDescriptor#addservice()`. + +The contact would be updated with `Model#updateContact()` and later saved into the `AddressBook` storage. + +==== Design Considerations + +===== Aspect 1: Cost storage +* *Alternative 1 (current choice)*: Store with `BigDecimal`. +** Pros: More precise representation, crucial to sensitive data like money. +** Cons: Difficult to retrieve value for display and parsing. +* *Alternative 2*: Store with `Double`. +** Pros: Easier to retrieve value for display and parsing. +** Cons: Less precise representation, may lose accuracy. + +===== Aspect 2: Updating of Service request +* *Alternative 1 (current choice)*: No updating allowed. Each request type can only be entered once. +** Pros: Easy to implement. +** Cons: Users who made a mistake while updating would have to delete and add the entire contact to the database. +* *Alternative 2*: Update with the same `addservice` command. +** Pros: Users can still update without much hassle. +** Cons: Users might accidentally overwrite the wrong data. +* *Alternative 3*: Update with the `update` command. +** Pros: Users can update with less possibility of overwriting the wrong data. +** Cons: Difficult to implement. `UpdateCommand` would have too many command variations. +In addition, both the service cost and type has to be stated in the command. + +// end::addservice[] + +=== Automatch feature + +==== Implementation + +// tag::automatchui[] +==== Search Result Display +After the user enters the `automatch` command into the `CommandBox`, individual `ServiceListPanel` s would be deployed to list the search results in a tabular form: + +.Automatch Table View +image::Ui.png[width"800"] + +===== Profile +The enquired contact's profile would be displayed on the right, so as to facilitate the user in picking the vendors or clients while keeping the requirements in mind. +The contact's data is extracted from `Contact#getUrlContactData()` and the text is set at their respective placeholders. + +===== Tabular View +The results would be listed from the most to the least relevant based on the enquired contact's price requests. +Users can then scroll through the list to view the other results in decreasing relevancy. + +===== Design Considerations +====== Aspect: How to display search results +* *Alternative 1 (current choice):* Present in a table +** Pros: Provides a bird's-eye view of all plausible vendors for the enquired client or clients for the enquired vendor +so that the user can pick the combination that best suits the client or vendor easily +** Cons: May have performance issues in terms of the extraction of data +* *Alternative 2:* Present in a list +** Pros: More efficient performance +** Cons: Users need to scroll through the list for each contact individually without knowing which service category +the contact falls under. +// end::automatchui[] + +// tag::list[] +=== List Feature +*_Heart²_* allows you view all the clients or the vendors with a simple command: `list`. + +When listing contacts, you would have to specify whether the contact is a client or a vendor +by prefixing it to list: + +* `client list` +* `vendor list` + +Below shows an example of how listing all clients works: + +._The UI showing how to list all clients._ +image::ListAllClients.png[width="800"] + +{empty} + + +Furthermore, you are also able to add keywords after the list to do filtering, and each keyword is specified to +belong to a category and only contacts which contains all of the keywords in their respective categories will be shown. + +[NOTE] +==== +Categories include: + +* `n/ NAME` +* `p/ PHONE_NUMBER` +* `e/ EMAIL_ADDRESS` +* `a/ ADDRESS` +* `t/ TAGS` +==== + +Below shows an example of how list filtering works: + +._The UI showing list filtering._ +image::ListClientsWithKeywords.png[width="800"] + +{empty} + + +==== Implementation + +The keywords from the command to be used for filtering is parsed by the `ListCommandParser` into a `ContactInformation` +and passed to a `Predicate` to be used for filtering. The `Predicate` is implemented as `ContactContainsKeywordsPredicate`. + +Below is a sequence diagram showing the creation of the `ListCommand`. + +._The Sequence Diagram of the creation of a list command._ +image::ListSequenceDiagram1.png[width="800"] + +{empty} + + +We use a `FilteredList` and pass the combination of 2 `Predicates` into it, one to filter the type of contact, +clients or vendors and the other is to filter by keywords, which is the `ContactContainsKeywordsPredicate` from the `ListCommandParser`. + +Below is a sequence diagram showing the execution of the `ListCommand`. + +._The Sequence Diagram of the execution of a list command._ +image::ListSequenceDiagram2.png[width="800"] + +==== Design considerations + +[none] +==== Aspect 1: Substring Matching or Word Matching +* *Alternative 1 (current choice):* Substring matching. +** Pros: Users would be able to view a wider range of results that matches the substring they have given. Easier to use. +** Cons: Irrelevant results might not be filtered away if they contain the substring. +* *Alternative 2:* Word matching. +** Pros: Guarantees that no irrelevant results are shown. +** Cons: Relevant results that have a small difference in the wording will be filtered away and not shown. + +[none] +==== Aspect 2: Categorised or Non-categorised keywords +* *Alternative 1 (current choice):* Categorised keywords. +** Pros: Users are able to specify which keywords they want to search for in which category. +Gives better control over the searching. +** Cons: Users have to follow a specific format to type the keywords. +* *Alternative 2:* Non-categorised keywords. +** Pros: User can type in the keywords in any order they want. Easier to use. +** Cons: Irrelevant results that contains the keywords will be shown. + +[none] +==== Aspect 3: All Match or Any Match +* *Alternative 1 (current choice):* All match. +** Pros: Users can specify what they want to search for and filter out all irrelevant results. +** Cons: Users are not able to search for multiple things, when they only require one of them to match. +* *Alternative 2:* Any match. +** Pros: Users are able to obtain a wider search result. Easier to use. +** Cons: Irrelevant results that contains only one or a few keywords will be shown as well. + +// end::list[] + +=== Finding matches between clients and vendors + + +The application boasts matchmaking features that reduces the (once-laborious) task of matching vendors a single command. + +==== High level design + +._High level overview of how auto-matching works_ +image::auto-matching.png[width:"800"] + +1. On invocation, the auto-matchmaking algorithm functionally maps all service requirements from a Client into predicates for performing the first step of filtering the Vendors. +2. The vendors are then sorted by a fair ranking algorithm to ensure even distribution of jobs between Vendors. + +==== Design considerations + +===== Aspect: How to fairly distribute jobs between vendors +* *Alternative 1 (current choice):* Pure random matching +** Pros: Fair at every selection round, easy implementation +** Cons: Even job distribution not guaranteed +* *Alternative 2:* Round robin +** Pros: Even job distribution guaranteed +** Cons: Requires keeping count of jobs allocated for each vendor +* *Alternative 3:* Review/ranking-based distribution +** Pros: Fair and rewards good performance +** Cons: Difficult to fine-tune ranking algorithm + == Documentation +image::documentationheader.png[width="400"] We use asciidoc for writing documentation. @@ -378,7 +1007,7 @@ Here are the steps to convert the project documentation files to PDF format. . Within Chrome, click on the `Print` option in Chrome's menu. . Set the destination to `Save as PDF`, then click `Save` to save a copy of the file in PDF format. For best results, use the settings indicated in the screenshot below. -.Saving documentation as PDF files in Chrome +._Saving documentation as PDF files in Chrome_ image::chrome_save_as_pdf.png[width="300"] [[Docs-SiteWideDocSettings]] @@ -457,6 +1086,9 @@ The SE-EDU team does not provide support for modified template files. [[Testing]] == Testing +image::testingheader.png[width="320"] + +Tests ensure that your code runs as expected. This section shows how you can run tests to test this application thoroughly. === Running Tests @@ -506,6 +1138,9 @@ e.g. `seedu.address.logic.LogicManagerTest` * Solution: Execute Gradle task `processResources`. == Dev Ops +image::devopsheader.png[width="320"] + +DevOps is an approach to include automation and event monitoring at all steps of the software build. This section documents the tools and methods we used to ensure a high quality code production. === Build Automation @@ -541,6 +1176,8 @@ b. Require developers to download those libraries manually (this creates extra w [appendix] == Suggested Programming Tasks to Get Started +image::appendixaheader.png[width="320"] + Suggested path for new programmers: 1. First, add small local-impact (i.e. the impact of the change does not go beyond the component) enhancements to one component at a time. Some suggestions are given in <>. @@ -818,83 +1455,406 @@ See this https://github.com/se-edu/addressbook-level4/pull/599[PR] for the step- [appendix] == Product Scope +image::appendixbheader.png[width="320"] + *Target user profile*: +* has a need to plan for events (weddings) * has a need to manage a significant number of contacts +* has a need to link contacts together * prefer desktop apps over other types * can type fast * prefers typing over mouse input * is reasonably comfortable using CLI apps -*Value proposition*: manage contacts faster than a typical mouse/GUI driven app +*Value proposition*: simplify the process of wedding management for the user, his clients and vendors [appendix] == User Stories +image::appendixcheader.png[width="320"] + Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*` [width="59%",cols="22%,<23%,<25%,<30%",options="header",] |======================================================================= |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 +|`* * *` |on-task project manager |add new clients with the type of services they request for |get the required vendors for the event accordingly -|`* * *` |user |add a new person | +|`* * *` |thoughtful project manager |add new vendors with the type of services they can offer and their costs |match the vendors to the clients accordingly -|`* * *` |user |delete a person |remove entries that I no longer need +|`* * *` |efficient project manager |search the database for the vendor that best suits the requirements based on filters |find the most suitable vendor for my clients -|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +|`* * *` |goal-driven project manager |be able to set individual checkpoints and reminders for the many components that a project may have |have a clearer picture on the progress of all the different projects -|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +|`* * *` |flexible project manager |update the database of vendors’ data |have an up-to-date database that accurately reflects my vendors -|`*` |user with many persons in the address book |sort persons by name |locate a person easily -|======================================================================= +|`* * *` |busy project manager |easily see all unserviced clients |I can quickly complete assigning vendors to them + +|`* * *` |organised project manager |view the availability of my vendors |I will not assign vendors to clients when they are unavailable + +|`* * *` |responsible project head |provide authentication for the project managers and staff |our clients’ and vendors’ data are only accessible by those who has access to them + +|`* * *` |organised project manager |be able to archive previous projects in a separate location |they would not clutter my workspace but would still be available for review in the future + +|`* *` |organised project manager|access clients and vendors separately |I can look through their data more efficiently + +|`* *` |modular project manager|offer packages to clients |clients with no particular preferences can be attended to efficiently + +|`*` |efficient project manager |create templates |I can easily serve customers of similar request types -_{More to be added}_ +|`*` |customer-first project manager |have a ratings and feedback system given by clients for the vendors |I can sieve out the better vendors for future clients + +|`*` |profit-motivated marketing head |calculate the rough estimate of the cost of each project |source for vendors that would maximise my profits +|======================================================================= [appendix] == Use Cases +image::appendixdheader.png[width="320"] -(For all use cases below, the *System* is the `AddressBook` and the *Actor* is the `user`, unless specified otherwise) +(For all use cases below, the *System* is the *_Heart²_* application, and the *Actor* is the `user`, unless specified otherwise) -[discrete] -=== Use case: Delete person +=== Use case: Add Client/Vendor + +*Preconditions*: User is logged in with a `SUPER_USER` account. + +*MSS* + +1. User requests to add a new Client/Vendor +2. System adds the new Client/Vendor into the database ++ +Use case ends. + +*Extensions* + +[none] +* 1a. The new Client/Vendor's syntax is not entered correct. ++ +[none] +** 1a1. System shows a feedback to the user that the Client/Vendor was not entered correctly. ++ +Use case ends. + + +=== Use case: Update Client/Vendor + +*Preconditions*: User is logged in with a `SUPER_USER` account. + +*MSS* + +1. User requests to update an existing Client/Vendor +2. System updates the existing Client/Vendor according to the User's requests ++ +Use case ends. + +*Extensions* + +[none] +* 1a. The Client/Vendor does not exist. ++ +[none] +** 1a1. System shows a feedback to the user that the Client/Vendor does not exist. ++ +Use case ends. + + +=== Use case: Delete Client/Vendor + +*Preconditions*: User is logged in with a `SUPER_USER` account. + +*MSS* + +1. User requests to delete an existing Client/Vendor +2. System deletes the Client/Vendor specified ++ +Use case ends. + +*Extensions* + +[none] +* 1a. The Client/Vendor does not exist. ++ +[none] +** 1a1. System shows a feedback to the user that the Client/Vendor does not exist ++ +Use case ends. + +=== Use case: Login + +*Preconditions*: User is logged out. + +*MSS* + +1. User requests to log in with his username and password +2. System validates the information entered and allows the user access to the System +3. User is successfully logged in ++ +Use case ends. + +*Extensions* + +[none] +* 1a. User enters an incorrect username + ++ +[none] +** 1a1. The system display an error message and prompts the user to re-enter his username ++ +[none] +** Use case resumes from step 1. + +[none] +* 1b. User enters an incorrect password + ++ +[none] +** 1b1. The system will request the user to re-enter his password ++ +[none] +** 1b2. The user attempts to enter his password ++ +[none] +*** 1b2.1 The system determines that the password is incorrect and provides the option for user to retrieve his forgotten password ++ +[none] +** Steps 1b1 and 1b2 are repeated until the user enters his correct password ++ +[none] +** Use case resumes from step 3. + +=== Use case: Logout + +*Preconditions*: User is logged in. + +*MSS* + +1. User requests to logout from the System +2. System logs User out +3. User is successfully logged out ++ +Use case ends. + +=== Use case: Register an account + +*Preconditions*: User is logged in with a `SUPER_USER` account. + +*MSS* + +1. User requests to register a new account +2. System validates the information entered and register the new account +3. User has successfully register a new account ++ +Use case ends. + +*Extensions* + +[none] +* 1a. User is not a `SUPER_USER`. ++ +[none] +** 1a1. System rejects the command to register a new account and feedback to the user that he is not a `SUPER_USER`. ++ +Use case ends. + +* 1b. User types in an invalid username. ++ +[none] +** 1b1. System prompts the User the correct format of the command that can be used. ++ +Use case ends. + +* 1c. User types in an invalid password. ++ +[none] +** 1c1. System prompts the User the correct format of the password that can be used. ++ +Use case ends. + +* 1d. User types in a username that already exists. ++ +[none] +** 1d1. System prompts the User that the username has been taken and suggest the User to choose another username. ++ +Use case ends. + +=== Use case: Changing an existing account password + +*Preconditions*: User is logged in. + +*MSS* + +1. User requests to change the password of his account +2. System validates the information entered and change the user password. +3. User's password is successfully updated. ++ +Use case ends. + +*Extensions* + +[none] +* 1a. User's old password is typed in wrongly. ++ +[none] +** 1a1. System rejects the command to change the user's password and feedback to the user that his old password was typed in wrongly. ++ +Use case ends. + +[none] +* 1b. User new password is invalid format. ++ +[none] +** 1a1. System prompts the user that his password is not a valid password and proceed to tell the user whether he has entered an empty password or a password with space. ++ +Use case ends. + +=== Use case: List all the Clients or Vendors + +*Preconditions*: User is logged in with a `SUPER_USER` account. *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 enters the list command and requests to view either all the Clients, or all the Vendors. +2. System returns either a list with all the Clients' information, or all the Vendors' information. + Use case ends. *Extensions* [none] -* 2a. The list is empty. +* 2a. There is no Client or no Vendor available ++ +[none] +** 2a1. System returns an empty list. + + Use case ends. -* 3a. The given index is invalid. +=== Use case: Filter and show Client’s or Vendor’s info according to the filter + +*Preconditions*: User is logged in with a `SUPER_USER` account. + +*MSS* + +1. User enters the list command and requests to view either Client’s or Vendor’s information with some +keywords provided indicated by prefixes. +2. The System displays a list of Clients or Vendors whose information matches what was provided. + +Use case ends. + +*Extensions* + [none] -** 3a1. AddressBook shows an error message. +* 1a. User enters a prefix that does not exist. + -Use case resumes at step 2. +[none] +** 1a1. System prompts the User the correct format of the command and prefixes that can be used. -_{More to be added}_ +* 1b. User enters an empty prefix. ++ +[none] +** 1b1. System prompts the User the correct format of the command and prefixes that can be used. + ++ +Use case ends. + +// tag::addserviceuc[] +=== Use Case: Adding a new service request + +*Preconditions*: User is logged in with a `SUPER_USER` account. + +*Guarantees*: + +* Service would be added to the specified contact only if the command is successful. +* The new service addition will not wipe out the previous service additions. + +*MSS* + +1. User enters the add service command with the specified client or vendor, along with the service type and price. +2. System adds the service to the contact and displays the result of the command in the result display. + +Use case ends. + +*Extensions* + +[none] +* 1a. System detects an invalid ID. +[none] +** 1a1. System prompts User that the ID is invalid. +** Use case ends. +* 1b. System detects an invalid service type. +[none] +** 1b1. System prompts User with the available service types. +** Use case ends. +* 1c. System detects an invalid service cost. +[none] +** 1c1. System prompts User with the cost format requirements. +** Use case ends. +// end::addserviceuc[] + +=== Use case: Match the most suitable Vendor to a Client's needs + +*Preconditions*: User is logged in with a `SUPER_USER` account. + +*MSS* + +1. User attempts to match a Client's need to an available Vendor +2. System matches a Vendor that it deemed the most suitable to the Client ++ +Use case ends. + +*Extensions* + +[none] +* 1a. The Client has no need. That is to say, the Client is not looking for any Vendor ++ +[none] +** 1a1. System recognises that the Client has no need, and return a message to feedback to the User ++ +Use case ends. +[none] ++ +* 2a. There is no Vendor available that matches the Client's need ++ +[none] +** 2a1. System feedback to the User that no Vendor is available for the current Client's need ++ +Use case ends. [appendix] == Non Functional Requirements +image::appendixeheader.png[width="320"] + +=== Availability +. Application should work on any <> as long as it has Java `9` or higher installed. +. Application should only be available for Wedding Managers with login credentials +. Application should be available 24hrs everyday without down time +. Data stored into the Application should be available to Users without corruption + +=== Performance +. Application should be able to hold up to 1000 Clients and 1000 Vendors without a noticeable sluggishness in performance for typical usage. -. Should work on any <> as long as it has Java `9` or higher installed. -. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +=== Usability +. Application should be intuitive and easy to use for users after following the User Guide . 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. +. A user without any technical knowledge should be able to use the program efficiently with the help of the user guide. + +=== Reliability +. Application should be able to match Vendor to Client's needs correctly. +. Application should be able to perform all the commands without fail. + +=== Scalability +. Application should be able to scale automatically even after reaching 1000 Clients or 1000 Vendors. +. Huge number of Clients and Vendors's data may cause some waiting time for commands to process, but Application should still be able to execute all the commands without fail. + +=== Data Integrity +. Only authorized Users with specific login credentials should be able to add, update, or delete data directly from the Application. +. All monetary amounts should be accurate to 2 decimal places. + -_{More to be added}_ [appendix] == Glossary +image::appendixfheader.png[width="320"] [[mainstream-os]] Mainstream OS:: Windows, Linux, Unix, OS-X @@ -902,38 +1862,38 @@ Windows, Linux, Unix, OS-X [[private-contact-detail]] Private contact detail:: A contact detail that is not meant to be shared with others -[appendix] -== Product Survey - -*Product Name* - -Author: ... +[[client]] Client:: +The Client is the primary receiver and the party that requires the services of the Project Manager and Vendors. The Client puts up requests to the Project Manager to handle their wedding event. -Pros: +[[vendor]] Vendor:: +The Vendors are what the Project Manager needs to connect his Clients to, to fulfil the Clients' needs and wants. Vendors provide goods and services required for the clients’ weddings and they depend on the Project Manager to look for suitable Clients. -* ... -* ... +[[project-manager]] Project Manager:: +The User of the application; the Project Manager of a wedding planning company that needs to engage his Clients with his Vendors, managing a large amount of wedding requests at a time. -Cons: - -* ... -* ... +[[event]] Event:: +A single request related to the wedding that the Client expects. The Project Manager plans for and provide this request from a Vendor. The event encapsulates many different services, such as: wedding photography, formal wedding attire rental, banquet catering, invitation printing, and many others. [appendix] == Instructions for Manual Testing +image::appendixgheader.png[width="320"] Given below are instructions to test the app manually. [NOTE] These instructions only provide a starting point for testers to work on; testers are expected to do more _exploratory_ testing. +For all commands except for `login`, `help`, and `exit`, user is expected to have already logged in to the application successfully and be at the main screen GUI page, not the login GUI page. + +[NOTE] +For all `add`, `update`, `delete`, `clear`, and `register account` commands, the account that executes it must be a `SUPER_USER` account. -=== Launch and Shutdown +=== Launch the application . Initial launch .. Download the jar file and copy into an empty folder .. Double-click the jar file + - Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + Expected: Shows the login page GUI. The window size may not be optimum. . Saving window preferences @@ -941,26 +1901,152 @@ These instructions only provide a starting point for testers to work on; testers .. Re-launch the app by double-clicking the jar file. + Expected: The most recent window size and location is retained. -_{ more test cases ... }_ +=== Shutdown the application + +. Exit the application with a command + +.. Test case: `exit` + + Expected: The GUI screen closes and the application exits. + +. Exit the application by clicking on the GUI close button or force quitting the app (e.g. `alt` + `f4` on Windows, and `cmd` + `q` on Mac) + +.. Expected: The GUI screen closes and the application exits. + +=== Adding a contact + +. Adding a contact in Heart² + +.. Test case: ` add n/Test user e/test@example.com a/test address p/11111111 t/test` + + Expected: The contact is added to either the `client` or `vendor` list, depending on the contact type specified in the command. Timestamp in the status bar is updated. +.. Test case: ` add n/Test two user e/test a/test address 2 p/22222222 t/test` + + Expected: No contact is added. Error details are shown in the status message. Status bar remains the same. +.. Other incorrect `add` commands to try: commands missing required fields, adding a duplicate contact (same name, contact type and email address or phone number). + + Expected: Similar to previous. + +=== Deleting a contact + +. Deleting a contact in Heart² + +.. Prerequisites: There are contacts in the list you are trying to delete (`client` or `vendor` list). +.. Test case: `#1 delete` + + Expected: The contact with ID #1 is deleted from the list. Details of the deleted contact are shown in the status message. Timestamp in the status bar is updated. +.. Test case: `#0 delete` + + Expected: No contact is deleted. Error details are shown in the status message. Status bar remains the same. +.. Other incorrect `delete` commands to try: `3 delete`, `#x delete` (where x is not a contact's id), `delete #1` + + Expected: Similar to previous. + +=== Updating a contact + +. Updating a contact that is present in Heart² + +.. Prerequisites: There are contacts to be updated. +.. Test case: `#1 update n/Test 2` + + Expected: The contact with ID #1 has its name updated in Heart². Details of the updated contact are shown in the status message. Timestamp in the status bar is updated. +.. Test case: `#0 update e/test@example.com` + + Expected: No contact is updated. Error details are shown in the status message. Status bar remains the same. +.. Other incorrect `update` commands to try: updating a contact to another contact already present in Heart² (same name, contact type and email address or phone number). + + Expected: Similar to previous. -=== Deleting a person +=== Viewing a contact -. Deleting a person while all persons are listed +. Viewing a contact that is present in Heart² -.. Prerequisites: List all persons using the `list` command. Multiple persons in the list. -.. 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. -.. Test case: `delete 0` + - Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. -.. Other incorrect delete commands to try: `delete`, `delete x` (where x is larger than the list size) _{give more}_ + +.. Prerequisites: The contact to be viewed is present in Heart². +.. Test case: `#1 view` + + Expected: The contact with ID #1 has its information displayed on the panel on the right of Heart². The contact card on the panel on the left of Heart² is selected. +.. Test case: `#0 view` + + Expected: The contact card on the panel on the left is empty. Error details are shown in the status message. Status bar remains the same. +.. Other incorrect `view` commands to try: `client#1 view asdf`, `client view`. + Expected: Similar to previous. -_{ more test cases ... }_ +=== Logging in + +. Log in to the application on new launch, or on log out. + +.. Prerequisites: User must not already been logged in. +.. Test case: `login u/rootUser p/rootPassword` + + Expected: Successfully logged in as rootUser. Login GUI is closed, and the main screen GUI is shown. Your username and your account role (either a super_user or read_only_user) are shown at the footer of the app. +.. Test case: `login u/YourCreatedUsername p/YourCreatedPassword` + + Expected: Successfully logged in as YourCreatedUsername. Login GUI is closed, and the main screen GUI is shown. Your username and your account role (either a super_user or read_only_user) are shown at the footer of the app. +.. Incorrect commands to try: `login u/YourCreatedUsername p/YourOldPassword`, `login u/YourCreatedUsername p/WrongPassword`, `login u/WrongUsername p/YourCreatedPassword` + + Expected: Failed to log in. GUI remains on the same page, with a feedback saying that the username or password is wrong. + +. Log in to the application after logging in. + +.. Prerequisites: User must have already logged in successfully and on the main screen GUI page. +.. Test case: `login u/rootUser p/rootPassword` + + Expected: Unknown command. + +=== Logging out + +. Log out of the application + +.. Test case: `logout` + + Expected: Successfully logged out. Main screen GUI is closed, and login screen GUI appears. + +=== Registering a new account + +. Register a new account that can be used as login credentials subsequently. + +.. Prerequisites: User account must be a `SUPER_USER`. +.. Test case: `register account u/newUserName p/newPassword r/superuser` + + Expected: Screen GUI feedback to say that you have successfully registered an account. You can now `logout` and log in with `login u/newUserName p/newPassword`. +.. Test case: `register account u/us3r p/p@ssw0rD r/readonlyuser` + + Expected: Screen GUI feedback to say that you have successfully registered an account. You can now `logout` and log in with `login u/us3r p/p@ssw0rD`. +.. Incorrect commands to try: `login u/ p/password r/superuser`, `login u/username p/ r/readonlyuser` (basically cannot contain empty field in any of u/ p/ or r/ parameter), `login u/usernameAlreadyExist p/password r/superuser` + +=== Changing your account password + +. Change your current account password to a new password. + +.. Prerequisites: You must know your old password. +.. Test case: `change password o/rootPassword n/newPassword` + + Expected: Screen GUI feedback to say that you have successfully changed your password. You can now `logout` and log in with `login u/rootUser p/newPassword`. +.. Incorrect commands to try: `changepassword o/rootPassword n/newPassword`, `change password o/WrongOldPassword n/newPasword`, `change password o/rootPassword n/` + +=== Listing contacts + +. List all clients. + +.. Test case: `client list` + + Expected: Screen GUI feedback to say that all client(s) listed. You can further verify whether each contact is a client. + +. List all vendors. + +.. Test case: `vendor list` + + Expected: Screen GUI feedback to say that all vendor(s) listed. You can further verify whether each contact is a vendor. + +. List with parameters. +.. Test case: `client list n/... p/... e/... a/... t/...` + + Expected: List all clients that matches the parameters provided. Test with different combinations of parameters and invalid ones to verify functionality of list. +.. Test case: `vendor list n/... p/... e/... a/... t/...` + + Expected: List all vendors that matches the parameters provided. Test with different combinations of parameters and invalid ones to verify functionality of list. + +// tag::addservicemanual[] +=== Adding a new service request + +. Adding a new service to a client or vendor with no service requests + +.. Prerequisites: You must create a new client or vendor with no service requests +.. Test case: `#1 addservice s/photographer c/1000.00` + + Expected: Service is added to the contact. Details of the service and the contact's name shown in status message. + You can now `view` the contact to rectify that the service is added with `#1 view`. + This would display the contact's information under "Service Requested" in the `BrowserPanel` on the right. +.. Incorrect commands to try: `#1 addservice s/florist c/1000.00`, `#1 addservice s/photographer c/1000`, `#1 addservice s/photographer c/$1000.00` -=== Saving data +. Adding a new service to a client or vendor with existing service requests -. Dealing with missing/corrupted data files +.. Prerequisites: You must have a client or vendor with at least one existing service request, that is not of `photographer` type +.. Test case: `#1 addservice s/photographer c/1000.00` + + Expected: Service is added to the contact. Details of the service and the contact's name shown in status message. + You can now `view` the contact to rectify that the service is added with `#1 view`. + This would display the contact's information under "Service Requested" in the `BrowserPanel` on the right, in addition to the existing services listed previously. +.. Incorrect commands to try: `#1 addservice s/florist c/1000.00`, `#1 addservice s/photographer c/1000`, `#1 addservice s/photographer c/$1000.00` -.. _{explain how to simulate a missing/corrupted file and the expected behavior}_ +. Adding a duplicate service to a client or vendor -_{ more test cases ... }_ +.. Prerequisites: You must have a client or vendor with a `photographer` type service request +.. Test case: `#1 addservice s/photographer c/1000.00` + + Expected: Service is not added to the client. Duplicate service message shown in status message. + You can now `view` the contact to rectify that the service is not overwritten with `#1 view`. +// end::addservicemanual[] diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 7e0070e12f49..0d42112c36cf 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - User Guide += Heart² - Enterprise Wedding Management System :site-section: UserGuide :toc: :toc-title: @@ -11,250 +11,513 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: +:warning-caption: :warning: +:experimental: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4 +:repoURL: https://github.com/CS2103-AY1819S1-F10-3/main/ + +image::userguide.png[width="700"] -By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` +By: `Team Heart²` Since: `Aug 2018` Last updated: `November 2018` Licence: `MIT` == Introduction +image::intropicture.png[width="256"] -AddressBook Level 4 (AB4) is for those who *prefer to use a desktop app for managing contacts*. More importantly, AB4 is *optimized for those who prefer to work with a Command Line Interface* (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB4 can get your contact management tasks done faster than traditional GUI apps. Interested? Jump to the <> to get started. Enjoy! +Welcome to *_Heart²_*! *_Heart²_* is a desktop software for wedding planning agencies to efficiently manage clients' and agency companies' profiles. +You can find suitable vendors providing services for couples using just a few keystrokes with our enterprise feature set. Jump over to <> to get started. == Quick Start +This section provides a quick overview to get you started with the application. + . Ensure you have Java version `9` or later installed in your Computer. -. Download the latest `addressbook.jar` link:{repoURL}/releases[here]. -. Copy the file to the folder you want to use as the home folder for your Address Book. -. Double-click the file to start the app. The GUI should appear in a few seconds. +. Download the latest `heart2.jar` link:{repoURL}/releases[here]. +. Copy the file to the folder you want to use as the home folder for your Wedding Planner. +. Double-click the file to start the app. The GUI for the login page should appear in a few seconds: + -image::Ui.png[width="790"] +image::UiLoginDiagram.png[width="790"] + . Type the command in the command box and press kbd:[Enter] to execute it. + e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. -. Some example commands you can try: - -* *`list`* : lists all contacts -* **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : adds a contact named `John Doe` to the Address Book. -* **`delete`**`3` : deletes the 3rd contact shown in the current list -* *`exit`* : exits the app - . Refer to <> for details of each command. +. After logging in, the GUI for the main page should appear: ++ +image::Ui.png[width="790"] [[Features]] == Features -==== -*Command Format* +image::format.png[width="256"] + +The following format is consistent for all the commands listed in this section. + +* 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 in `add n/John Doe`. +* Items in square brackets are optional. E.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. +* Items with `…`​ after them can be used multiple times, including zero times. E.g. `[t/TAG]...` can be used as `{nbsp}` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. +* Parameters can be in any order. E.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. +* `` is to replaced with either `client` *or* `vendor`. +* `` is to replaced with a valid ID, and only positive integers are recognised as an ID. + +* An example table shows the usage with actual data for a particular command. Below shows one instance of such a table. + +._An example table for login command_ +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`login u/rootUser p/rootPassword` +|===== + +image::callouts2.png[width="256"] + +Callouts are rectangular boxes with an icon and words to point out various types of information. Below are 3 callouts that will be used throughout this document: + +[NOTE] +This represents a *note*. A note indicates additional important information. Be sure to read these notes as they might be applicable to you! + +[TIP] +This represents a *tip*. A tip denotes something that is often handy, and good for you to know. Tips are often less crucial, and you can choose to skip them. + +[WARNING] +This represents a *warning*. A warning denotes something of crucial importance, and you should be extremely cautious when reading the statement. + +{empty} + -* Words in `UPPER_CASE` are the parameters to be supplied by the user e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. -* Items in square brackets are optional e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. -* Items with `…`​ after them can be used multiple times including zero times e.g. `[t/TAG]...` can be used as `{nbsp}` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. -* Parameters can be in any order e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. -==== +image::commands.png[width="256"] + +*_Heart²_* is designed with an intuitive command syntax for your ease of use. +The following section documents all the commands available for you in this application. + +[NOTE] +Only `help`, `login`, and `exit` commands are available prior to logging in. The rest of the commands are only available after logging into the application, and might be marked as Unknown Command prior to logging in. + +[NOTE] +For all commands that require an `#ID`, your ID must be a valid ID that matches the entries that you have. This user guide's ID `#123` may not be available in your data. Change the ID accordingly to the ID that you wish for a command. === Viewing help : `help` +Opens a new window that contains the user guide to help you find out any information you need. + Format: `help` -=== Adding a person: `add` +[NOTE] +You cannot `undo` a help. + +=== Logging in : `login` + +Securely logs you in to access the system. By default, a root account with `SUPER_USER` privilege is provided, using the username `rootUser` and password `rootPassword`. + +Format: `login u/USERNAME p/PASSWORD` + +[NOTE] +Condition of use: You must not be logged in. Otherwise, it will be an unknown command. + +[NOTE] +You cannot `undo` a login. + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`login u/rootUser p/rootPassword` +|===== + +=== Exiting the program : `exit` + +Exits the program. + +Format: `exit` + +=== Logging out : `logout` + +Securely logs you out of the system. + +Format: `logout` + +[NOTE] +You cannot `undo` a logout. + +=== Registering a new account : `register account` + +Register a new account for this application. You can only register a new account after logging in with a `SUPER_USER` account. By default, `rootUser` is a default account with `SUPER_USER` privilege. + +Format: `register account u/USERNAME p/PASSWORD r/ROLE` + +[NOTE] +`r/ROLE`: either `r/superuser` or `r/readonlyuser` to create a `SUPER_USER` account or `READ_ONLY_USER` account respectively. + +[NOTE] +It may sound counter-intuitive to require an account before registering a new account. We make this requirement as only authorised personal should be given an account. Ideally, the owner of the application should dictate the account given to employees by helping them register an account. + +[NOTE] +You cannot `undo` registering a new account. + + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`register account u/myNewUsername p/p@ssw0rd r/superuser` +|===== + +[WARNING] +Username and password cannot be empty, or contain spaces. + +[WARNING] +Make sure your password is typed correctly. There is no confirmation prompt once you execute the command. + +=== Change the current password : `change password` + +Change your current account password from an old password to a new password. + +Format: `change password o/YOUR_OLD_PASSWORD n/YOUR_NEW_PASSWORD` + +[NOTE] +You cannot `undo` changing of password. + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`change password o/jf3nv n/j9y3nd` +|===== + +[WARNING] +New password should not be the same as old password, and it cannot be empty, or contain spaces. + +[WARNING] +Make sure your password is typed correctly. There is no confirmation prompt once you execute the command. + +=== Working with contacts + +There are two types of contacts supported by *_Heart²_*, namely `client` and `vendor`. -Adds a person to the address book + -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` +[TIP] +All commands pertaining to contacts start with either `client` or `vendor`. + +==== Adding a contact: `add` + +You can `add` a contact together with its particulars into *_Heart²_* by specifying its contact type first, that is, `client` or `vendor`. + +Format: ` add n/FULL_NAME p/PHONE_NUMBER e/EMAIL_ADDRESS a/HOME_ADDRESS [t/TAG]…​` + +[NOTE] +You cannot add a contact that already exists in the application. Two contacts are considered duplicates if they are of the same type (either both clients or both vendors), possess the same name, and either the same phone number or the same email address. + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client add n/John Doe p/87654321 e/johndoe@gmail.com a/123 Lorem Street, #45-67, Singapore 890123` +|===== + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor add n/Mary Jane p/98765432 e/maryjane@gmail.com a/123 Lorem Street, #67-89, Singapore 890321` +|===== [TIP] -A person can have any number of tags (including 0) +You can `undo` and `redo` adding of a contact! -Examples: +// tag::list[] +==== Listing and searching for contacts: `list` -* `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` +You can view all the contacts of a specific type in a list by specifying the contact type to be shown. -=== Listing all persons : `list` +Format: ` list` -Shows a list of all persons in the address book. + -Format: `list` +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client list` +|===== -=== Editing a person : `edit` +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor list` +|===== -Edits an existing person in the address book. + -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` +*_Heart²_* also supports searching via name, phone number, email, address and tags for you to quickly find your contacts. +To search, simply append your search parameters to the back of the original command. -**** -* 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. -**** +Only contacts that matches all the search parameters will be displayed to you in the form of a list. +Searching is done through substring matching, so you do not need to enter the full name, just part of the name will do. -Examples: +Format: ` list [n/FULL_NAME] [p/PHONE_NUMBER] [e/EMAIL_ADDRESS] [a/HOME_ADDRESS] [t/TAG]...` -* `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. +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client list n/John` +|===== -=== Locating persons by name: `find` +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor list p/98765432` +|===== +// end::list[] -Finds persons whose names contain any of the given keywords. + -Format: `find KEYWORD [MORE_KEYWORDS]` +=== Working with a specific contact -**** -* 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` -**** +Contacts in *_Heart²_* are assigned a unique ID each for you to refer to the contact at any point of time when using *_Heart²_*. -Examples: -* `find John` + -Returns `john` and `John Doe` -* `find Betsy Tim John` + -Returns any person having names `Betsy`, `Tim`, or `John` +[TIP] +Commands pertaining to one specific contact have an additional unique ID appended to the back of `client` or `vendor`. -=== Deleting a person : `delete` +[TIP] +These IDs are persistent for one session. Each restart of Heart reassigns IDs to contacts, effectively accounting for deleted contacts and compacting the IDs of your contacts. + +[NOTE] +IDs can be similar for `client` and `vendor`. However, since the contact type and ID come hand in hand, the contacts are still effectively unique! + +==== Viewing a contact: `view` -Deletes the specified person from the address book. + -Format: `delete INDEX` +You can `view` detailed information about a specific contact using its unique ID. +This information will be displayed on the right panel in *_Heart²_*. -**** -* 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, ... -**** +[TIP] +`view` shows you all the information regarding the specific client, which includes the name, phone number, email address, tags, residential (client) or office (vendor) address and services requested (client) or offered (vendor). -Examples: +[NOTE] +*Any* command following `view` will hide the information shown before on the panel on the right. -* `list` + -`delete 2` + -Deletes the 2nd person in the address book. -* `find Betsy` + -`delete 1` + -Deletes the 1st person in the results of the `find` command. +Format: `# view` -=== Selecting a person : `select` +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client#3 view` +|===== +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor#3 view` +|===== -Selects the person identified by the index number used in the displayed person list. + -Format: `select INDEX` +[TIP] +Clicking on the contact panels on the left of *_Heart²_*'s GUI corresponds to a `view` command for that contact! -**** -* Selects the person and loads the Google search page 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, ...` -**** +==== Deleting a contact: `delete` -Examples: +You can also `delete` a contact from *_Heart²_*, by specifying its unique ID. -* `list` + -`select 2` + -Selects the 2nd person in the address book. -* `find Betsy` + -`select 1` + -Selects the 1st person in the results of the `find` command. +Format: `# delete` -=== Listing entered commands : `history` +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client#123 delete` +|===== +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor#123 delete` +|===== -Lists all the commands that you have entered in reverse chronological order. + -Format: `history` +[TIP] +You can `undo` and `redo` deleting a contact! + +==== Updating a contact: `update` + +You can also `update` a contact's particulars, again by specifying its unique ID, followed by the updated fields. + +Format: `# update [n/FULL_NAME] [p/PHONE_NUMBER] [e/EMAIL_ADDRESS] [a/HOME_ADDRESS] [t/TAG]...` [NOTE] -==== -Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. -==== +You cannot update a contact to another contact that already exists in the application. Two contacts are considered duplicates if they are the same type (either both clients or both vendors), possess the same name, and either the same phone number or the same email address. -// tag::undoredo[] -=== Undoing previous command : `undo` +[NOTE] +When editing tags, adding of tags is not cumulative and existing tags of the contact will be removed. -Restores the address book to the state before the previous _undoable_ command was executed. + -Format: `undo` +[TIP] +You can remove all tags for a contact by updating the contact with `t/` (without specifying any tags after it)! + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client#123 update n/Jane Doe e/janedoe@gmail.com` +|===== +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor#123 update n/Bob Vans e/bobvans@gmail.com` +|===== + +[TIP] +You can `undo` and `redo` updating a contact! + +// tag::addservice[] +==== Adding a service: `addservice` + +You can add attributes of the services your clients require or vendors can provide by using the + `addservice` command specified by the contacts' unique IDs. + +[WARNING] +Every service can only be added *once*. Updating this service is not supported. [NOTE] -==== -Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). -==== +Specify the cost in _Singapore Dollars (SGD)_ to 2 decimal places, and exclude spacing and symbols (e.g. '$' ','). -Examples: +Format: `# addservice s/SERVICE_TYPE c/SERVICE_COST` -* `delete 1` + -`list` + -`undo` (reverses the `delete 1` command) + +Available service types for `SERVICE_TYPE`: -* `select 1` + -`list` + -`undo` + -The `undo` command fails as there are no undoable commands executed previously. +|======= +|`photographer` |`hotel` | `catering` |`dress` +|`ring` |`transport` | `invitation` | +|======= -* `delete 1` + -`clear` + -`undo` (reverses the `clear` command) + -`undo` (reverses the `delete 1` command) + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client#123 addservice s/photographer c/2000.00` +|===== -=== Redoing the previously undone command : `redo` +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor#123 addservice s/catering c/10000.00` +|===== -Reverses the most recent `undo` command. + -Format: `redo` +[TIP] +You can `undo` and `redo` adding a service to a contact! +// end::addservice[] + +==== Automatching for a client: `automatch` -Examples: +You can easily find vendors that can fulfil the request services with this command. -* `delete 1` + -`undo` (reverses the `delete 1` command) + -`redo` (reapplies the `delete 1` command) + +Format: `client# automatch` -* `delete 1` + -`redo` + -The `redo` command fails as there are no `undo` commands executed previously. +[NOTE] +It only shows you the vendors within the budget requirement that fulfils a particular service requirement of the client. -* `delete 1` + -`clear` + -`undo` (reverses the `clear` command) + -`undo` (reverses the `delete 1` command) + -`redo` (reapplies the `delete 1` command) + -`redo` (reapplies the `clear` command) + -// end::undoredo[] +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client#123 automatch` +|===== -=== Clearing all entries : `clear` +==== Automatching for a vendor: `automatch` -Clears all entries from the address book. + -Format: `clear` +You can easily find clients whose service requirements match the services offered by a vendor. -=== Exiting the program : `exit` +Format: `vendor# automatch` -Exits the program. + -Format: `exit` +[NOTE] +It only shows you the clients whose budgets can afford the service of the particular vendor. -=== Saving the data +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor#123 automatch` +|===== + +=== Listing entered commands: `history` + +Lists all the commands that you have entered in reverse chronological order for this particular session. Histories are deleted upon logging out or exiting the app. + +By default, the following commands will have their parameters censored when saving to history. This is because these commands contain sensitive information such as user password. + +* `login` +* `register account` +* `change password` + +The command `logout` will not be saved to history as the session has ended due to logging out. + +Format: `history` + +[NOTE] +Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. + +// tag::undoredo[] +=== Undo the previous command: `undo` -Address book data are saved in the hard disk automatically after any command that changes the data. + -There is no need to save manually. +Undo the commands that you have entered in chronological order for this particular session. Once you logout or exit the application, you cannot `undo` a command from the last session. -// tag::dataencryption[] -=== Encrypting data files `[coming in v2.0]` +Format: `undo` + +[NOTE] +The application will only undo commands that modifies the list of contacts: `add`, `update`, `delete`, `clear` + +[NOTE] +The application will show either the client list or vendor list corresponding to the command that was undone. -_{explain how the user can enable/disable data encryption}_ -// end::dataencryption[] +=== Redo the commands undone: `redo` + +Redo the commands that you have undone by undo in chronological order for this particular session. Once you logout or exit the application, you cannot `redo` a command from the last session. + +Format: `redo` + +[NOTE] +Commands that have been undone will be reset upon a `clear` command. + +[NOTE] +The application will show either the client list or vendor list corresponding to the command that was redone. + +=== Saving the data + +Address book data are saved in the hard disk automatically after any command that changes the data. + +There is no need for you to save manually. + +== Command summary + +Below is a summary of the commands available for you to use. + +*Before logging in* + +[cols="^30,^70", options="header"] +|=== +|FEATURE |FORMAT +|To get help | `help` +|To log in | `login u/USERNAME p/PASSWORD` +|To close the application | `exit` +|=== + +*After logging in* + +[cols="^30,^70", options="header"] +|=== +|FEATURE |FORMAT +|To register a new account | `register account u/USERNAME p/PASSWORD r/ROLE` +|To change your account password | `change password o/YOUR_OLD_PASSWORD n/YOUR_NEW_PASSWORD` +|To add a contact | ` add n/FULL_NAME p/PHONE_NUMBER e/EMAIL_ADDRESS a/HOME_ADDRESS [t/TAG]…​` +|To add a service requirement | `# addservice s/SERVICE_TYPE c/SERVICE_COST` +|To update a specific contact | `# update [n/FULL_NAME] [p/PHONE_NUMBER] [e/EMAIL_ADDRESS] [a/HOME_ADDRESS] [t/TAG]...` +|To list contacts that matches the inputs | ` list [n/FULL_NAME] [p/PHONE_NUMBER] [e/EMAIL_ADDRESS] [a/HOME_ADDRESS] [t/TAG]...` +|To find a match that fits a particular contact's requirements | `# automatch` +|To view a specific contact | `# view` +|To delete a specific contact | `# delete` +|To delete all contacts | `clear` +|To list all the commands entered in this session | `history` +|To undo the previous command | `undo` +|To redo the previous undone command | `redo` +|To get help | `help` +|To log out of your account | `logout` +|To close the application | `exit` +|=== + +*Coming in v2.0* + +[cols="^30,^70", options="header"] +|=== +|FEATURE |FORMAT +|To retrieve forgotten password | `forget password` +|To assigning vendors to clients | `client# assign vendor#` +|To assigning clients to vendors | `vendor# assign client#` +|=== == FAQ +image::faqpicture.png[width="400"] -*Q*: How do I transfer my data to another Computer? + +*[red]#Q*#: [red]#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 Address Book folder. -== Command Summary - -* *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` -* *Select* : `select INDEX` + -e.g.`select 2` -* *History* : `history` -* *Undo* : `undo` -* *Redo* : `redo` +*[red]#Q*#: [red]#What platform is this application available on?# + +*A*: This application is cross-platform, and can be used on both Windows and Mac OS. + +*[red]#Q*#: [red]#Is this application free-of-charge?# + +*A*: Yes, this application is open-source and can be used for free, even commercially. + +*[red]#Q*#: [red]#How can I report an issue?# + +*A*: You can raise an issue in the issue section and our team will get back to you as soon as possible. diff --git a/docs/diagrams/AddClientSequenceDiagram.pptx b/docs/diagrams/AddClientSequenceDiagram.pptx new file mode 100644 index 000000000000..2143ede00b4f Binary files /dev/null and b/docs/diagrams/AddClientSequenceDiagram.pptx differ diff --git a/docs/diagrams/AddServiceSequenceDiagram.pptx b/docs/diagrams/AddServiceSequenceDiagram.pptx new file mode 100644 index 000000000000..dd255b4ea4ca Binary files /dev/null and b/docs/diagrams/AddServiceSequenceDiagram.pptx differ diff --git a/docs/diagrams/HighLevelSequenceDiagrams.pptx b/docs/diagrams/HighLevelSequenceDiagrams.pptx index 38332090a79a..de840ceac7b7 100644 Binary files a/docs/diagrams/HighLevelSequenceDiagrams.pptx and b/docs/diagrams/HighLevelSequenceDiagrams.pptx differ diff --git a/docs/diagrams/ListSequenceDiagram1.pptx b/docs/diagrams/ListSequenceDiagram1.pptx new file mode 100644 index 000000000000..6fc84a52ee4b Binary files /dev/null and b/docs/diagrams/ListSequenceDiagram1.pptx differ diff --git a/docs/diagrams/ListSequenceDiagram2.pptx b/docs/diagrams/ListSequenceDiagram2.pptx new file mode 100644 index 000000000000..b01e6860b85d Binary files /dev/null and b/docs/diagrams/ListSequenceDiagram2.pptx differ diff --git a/docs/diagrams/LogicComponentSequenceDiagram.pptx b/docs/diagrams/LogicComponentSequenceDiagram.pptx index c5b6d5fad6e3..ed2a18660514 100644 Binary files a/docs/diagrams/LogicComponentSequenceDiagram.pptx and b/docs/diagrams/LogicComponentSequenceDiagram.pptx differ diff --git a/docs/diagrams/UiComponentClassDiagram.pptx b/docs/diagrams/UiComponentClassDiagram.pptx index 384d0a00e6ea..e8cc7cb95ac9 100644 Binary files a/docs/diagrams/UiComponentClassDiagram.pptx and b/docs/diagrams/UiComponentClassDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoSequenceDiagram.pptx b/docs/diagrams/UndoRedoSequenceDiagram.pptx index 5ccc1042caac..cf121e7dae49 100644 Binary files a/docs/diagrams/UndoRedoSequenceDiagram.pptx and b/docs/diagrams/UndoRedoSequenceDiagram.pptx differ diff --git a/docs/diagrams/~$UndoRedoSequenceDiagram.pptx b/docs/diagrams/~$UndoRedoSequenceDiagram.pptx new file mode 100644 index 000000000000..138f1a0b27a2 Binary files /dev/null and b/docs/diagrams/~$UndoRedoSequenceDiagram.pptx differ diff --git a/docs/images/AccountStorageClassDiagram.png b/docs/images/AccountStorageClassDiagram.png new file mode 100644 index 000000000000..0e88edaf43b3 Binary files /dev/null and b/docs/images/AccountStorageClassDiagram.png differ diff --git a/docs/images/AddClientSequenceDiagram.png b/docs/images/AddClientSequenceDiagram.png new file mode 100644 index 000000000000..a40254f634c8 Binary files /dev/null and b/docs/images/AddClientSequenceDiagram.png differ diff --git a/docs/images/AddServiceSequenceDiagram.png b/docs/images/AddServiceSequenceDiagram.png new file mode 100644 index 000000000000..0d30549880b0 Binary files /dev/null and b/docs/images/AddServiceSequenceDiagram.png differ diff --git a/docs/images/DeleteClientSdForLogic.png b/docs/images/DeleteClientSdForLogic.png new file mode 100644 index 000000000000..314eb4e60e02 Binary files /dev/null and b/docs/images/DeleteClientSdForLogic.png differ diff --git a/docs/images/DeletePersonSdForLogic.png b/docs/images/DeletePersonSdForLogic.png deleted file mode 100644 index 0462b9b7be6e..000000000000 Binary files a/docs/images/DeletePersonSdForLogic.png and /dev/null differ diff --git a/docs/images/ListAllClients.png b/docs/images/ListAllClients.png new file mode 100644 index 000000000000..c1fa26a09247 Binary files /dev/null and b/docs/images/ListAllClients.png differ diff --git a/docs/images/ListClientsWithKeywords.png b/docs/images/ListClientsWithKeywords.png new file mode 100644 index 000000000000..992ccf733028 Binary files /dev/null and b/docs/images/ListClientsWithKeywords.png differ diff --git a/docs/images/ListSequenceDiagram1.png b/docs/images/ListSequenceDiagram1.png new file mode 100644 index 000000000000..41ef327f2ed6 Binary files /dev/null and b/docs/images/ListSequenceDiagram1.png differ diff --git a/docs/images/ListSequenceDiagram2.png b/docs/images/ListSequenceDiagram2.png new file mode 100644 index 000000000000..0c3fcfcf1973 Binary files /dev/null and b/docs/images/ListSequenceDiagram2.png differ diff --git a/docs/images/ModelClassBetterOopDiagram.png b/docs/images/ModelClassBetterOopDiagram.png index 9ba8eb5e31d0..1dce14c7fd40 100644 Binary files a/docs/images/ModelClassBetterOopDiagram.png and b/docs/images/ModelClassBetterOopDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 9fb19078b859..81dc82c5828a 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/SDforDeletePerson.png b/docs/images/SDforDeletePerson.png deleted file mode 100644 index 1e836f10dcd8..000000000000 Binary files a/docs/images/SDforDeletePerson.png and /dev/null differ diff --git a/docs/images/SDforDeletePersonEventHandling.png b/docs/images/SDforDeletePersonEventHandling.png deleted file mode 100644 index ecec0805d32c..000000000000 Binary files a/docs/images/SDforDeletePersonEventHandling.png and /dev/null differ diff --git a/docs/images/SdForDeleteClient.png b/docs/images/SdForDeleteClient.png new file mode 100644 index 000000000000..a64015819276 Binary files /dev/null and b/docs/images/SdForDeleteClient.png differ diff --git a/docs/images/SdForDeleteClientEventHandling.png b/docs/images/SdForDeleteClientEventHandling.png new file mode 100644 index 000000000000..a42331e7d588 Binary files /dev/null and b/docs/images/SdForDeleteClientEventHandling.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5ec9c527b49c..fdbbc4519894 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 369469ef176e..7636bc3c0b74 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UiLoginDiagram.png b/docs/images/UiLoginDiagram.png new file mode 100644 index 000000000000..966ad79bc167 Binary files /dev/null and b/docs/images/UiLoginDiagram.png differ diff --git a/docs/images/UndoRedoSequenceDiagram.png b/docs/images/UndoRedoSequenceDiagram.png index 5c9d5936f098..d706ebb14333 100644 Binary files a/docs/images/UndoRedoSequenceDiagram.png and b/docs/images/UndoRedoSequenceDiagram.png differ diff --git a/docs/images/abouttheproject.png b/docs/images/abouttheproject.png new file mode 100644 index 000000000000..27ecded8e851 Binary files /dev/null and b/docs/images/abouttheproject.png differ diff --git a/docs/images/accountcreationdiagram.png b/docs/images/accountcreationdiagram.png new file mode 100644 index 000000000000..70b7e6fbeb67 Binary files /dev/null and b/docs/images/accountcreationdiagram.png differ diff --git a/docs/images/accountstoragediagram.png b/docs/images/accountstoragediagram.png new file mode 100644 index 000000000000..90f8448465a6 Binary files /dev/null and b/docs/images/accountstoragediagram.png differ diff --git a/docs/images/appendixaheader.png b/docs/images/appendixaheader.png new file mode 100644 index 000000000000..40b59bfc48ae Binary files /dev/null and b/docs/images/appendixaheader.png differ diff --git a/docs/images/appendixbheader.png b/docs/images/appendixbheader.png new file mode 100644 index 000000000000..c4d23ca3cf1b Binary files /dev/null and b/docs/images/appendixbheader.png differ diff --git a/docs/images/appendixcheader.png b/docs/images/appendixcheader.png new file mode 100644 index 000000000000..6308803ae9b4 Binary files /dev/null and b/docs/images/appendixcheader.png differ diff --git a/docs/images/appendixdheader.png b/docs/images/appendixdheader.png new file mode 100644 index 000000000000..575c74b5c82b Binary files /dev/null and b/docs/images/appendixdheader.png differ diff --git a/docs/images/appendixeheader.png b/docs/images/appendixeheader.png new file mode 100644 index 000000000000..a16ab411bd95 Binary files /dev/null and b/docs/images/appendixeheader.png differ diff --git a/docs/images/appendixfheader.png b/docs/images/appendixfheader.png new file mode 100644 index 000000000000..2d9869c159da Binary files /dev/null and b/docs/images/appendixfheader.png differ diff --git a/docs/images/appendixgheader.png b/docs/images/appendixgheader.png new file mode 100644 index 000000000000..392850d37131 Binary files /dev/null and b/docs/images/appendixgheader.png differ diff --git a/docs/images/appendixhheader.png b/docs/images/appendixhheader.png new file mode 100644 index 000000000000..0fab961dab0f Binary files /dev/null and b/docs/images/appendixhheader.png differ diff --git a/docs/images/auto-matching.png b/docs/images/auto-matching.png new file mode 100644 index 000000000000..2010b64552e2 Binary files /dev/null and b/docs/images/auto-matching.png differ diff --git a/docs/images/automatching.png b/docs/images/automatching.png new file mode 100644 index 000000000000..c152cdc4efdf Binary files /dev/null and b/docs/images/automatching.png differ diff --git a/docs/images/calloutpic.png b/docs/images/calloutpic.png new file mode 100644 index 000000000000..5ecfaa1fce06 Binary files /dev/null and b/docs/images/calloutpic.png differ diff --git a/docs/images/callouts2.png b/docs/images/callouts2.png new file mode 100644 index 000000000000..b65dc1653dc3 Binary files /dev/null and b/docs/images/callouts2.png differ diff --git a/docs/images/commands.png b/docs/images/commands.png new file mode 100644 index 000000000000..9940aad078be Binary files /dev/null and b/docs/images/commands.png differ diff --git a/docs/images/damithc.jpg b/docs/images/damithc.jpg deleted file mode 100644 index 127543883893..000000000000 Binary files a/docs/images/damithc.jpg and /dev/null differ diff --git a/docs/images/designheader.png b/docs/images/designheader.png new file mode 100644 index 000000000000..710410b06264 Binary files /dev/null and b/docs/images/designheader.png differ diff --git a/docs/images/developerguideexample.png b/docs/images/developerguideexample.png new file mode 100644 index 000000000000..d909e9cb6e01 Binary files /dev/null and b/docs/images/developerguideexample.png differ diff --git a/docs/images/developerguideheader.png b/docs/images/developerguideheader.png new file mode 100644 index 000000000000..d41fa6629797 Binary files /dev/null and b/docs/images/developerguideheader.png differ diff --git a/docs/images/devopsheader.png b/docs/images/devopsheader.png new file mode 100644 index 000000000000..6b16a88d18c3 Binary files /dev/null and b/docs/images/devopsheader.png differ diff --git a/docs/images/documentationheader.png b/docs/images/documentationheader.png new file mode 100644 index 000000000000..4d95b92ffc03 Binary files /dev/null and b/docs/images/documentationheader.png differ diff --git a/docs/images/dongsiji.png b/docs/images/dongsiji.png new file mode 100644 index 000000000000..74469ff486cf Binary files /dev/null and b/docs/images/dongsiji.png differ diff --git a/docs/images/eehooi.png b/docs/images/eehooi.png new file mode 100644 index 000000000000..eb04c07a4d39 Binary files /dev/null and b/docs/images/eehooi.png differ diff --git a/docs/images/exampleimage.png b/docs/images/exampleimage.png new file mode 100644 index 000000000000..b96d03f4714f Binary files /dev/null and b/docs/images/exampleimage.png differ diff --git a/docs/images/extract.png b/docs/images/extract.png new file mode 100644 index 000000000000..65aeb7d076f2 Binary files /dev/null and b/docs/images/extract.png differ diff --git a/docs/images/faqpicture.png b/docs/images/faqpicture.png new file mode 100644 index 000000000000..c1884f8510a0 Binary files /dev/null and b/docs/images/faqpicture.png differ diff --git a/docs/images/format.png b/docs/images/format.png new file mode 100644 index 000000000000..03b7faebe5b4 Binary files /dev/null and b/docs/images/format.png differ diff --git a/docs/images/guidefordevelopers.png b/docs/images/guidefordevelopers.png new file mode 100644 index 000000000000..e10edb0078ed Binary files /dev/null and b/docs/images/guidefordevelopers.png differ diff --git a/docs/images/icon-danger.png b/docs/images/icon-danger.png new file mode 100644 index 000000000000..938e89cdf424 Binary files /dev/null and b/docs/images/icon-danger.png differ diff --git a/docs/images/icon-notes.png b/docs/images/icon-notes.png new file mode 100644 index 000000000000..6b2dd865e729 Binary files /dev/null and b/docs/images/icon-notes.png differ diff --git a/docs/images/icon-tips.png b/docs/images/icon-tips.png new file mode 100644 index 000000000000..0d011bcaff4c Binary files /dev/null and b/docs/images/icon-tips.png differ diff --git a/docs/images/implementationheader.png b/docs/images/implementationheader.png new file mode 100644 index 000000000000..b0a72a08e7e1 Binary files /dev/null and b/docs/images/implementationheader.png differ diff --git a/docs/images/intropicture.png b/docs/images/intropicture.png new file mode 100644 index 000000000000..d428d041d765 Binary files /dev/null and b/docs/images/intropicture.png differ diff --git a/docs/images/lejolly.jpg b/docs/images/lejolly.jpg deleted file mode 100644 index 2d1d94e0cf5d..000000000000 Binary files a/docs/images/lejolly.jpg and /dev/null differ diff --git a/docs/images/liaujianjie.png b/docs/images/liaujianjie.png new file mode 100644 index 000000000000..ab878d2dc2d5 Binary files /dev/null and b/docs/images/liaujianjie.png differ diff --git a/docs/images/logotransparentbackground.png b/docs/images/logotransparentbackground.png new file mode 100644 index 000000000000..4564b5dbd8de Binary files /dev/null and b/docs/images/logotransparentbackground.png differ diff --git a/docs/images/m133225.jpg b/docs/images/m133225.jpg deleted file mode 100644 index fd14fb94593a..000000000000 Binary files a/docs/images/m133225.jpg and /dev/null differ diff --git a/docs/images/major.png b/docs/images/major.png new file mode 100644 index 000000000000..f7113cfad09b Binary files /dev/null and b/docs/images/major.png differ diff --git a/docs/images/minor.png b/docs/images/minor.png new file mode 100644 index 000000000000..bcb2d28d9921 Binary files /dev/null and b/docs/images/minor.png differ diff --git a/docs/images/nightyeti.png b/docs/images/nightyeti.png new file mode 100644 index 000000000000..48fb20ce44b4 Binary files /dev/null and b/docs/images/nightyeti.png differ diff --git a/docs/images/others.png b/docs/images/others.png new file mode 100644 index 000000000000..edf6214c84de Binary files /dev/null and b/docs/images/others.png differ diff --git a/docs/images/parta.png b/docs/images/parta.png new file mode 100644 index 000000000000..05249235a0a6 Binary files /dev/null and b/docs/images/parta.png differ diff --git a/docs/images/partb.png b/docs/images/partb.png new file mode 100644 index 000000000000..88778f16586a Binary files /dev/null and b/docs/images/partb.png differ diff --git a/docs/images/partc.png b/docs/images/partc.png new file mode 100644 index 000000000000..621f675c719e Binary files /dev/null and b/docs/images/partc.png differ diff --git a/docs/images/projectportfolio.png b/docs/images/projectportfolio.png new file mode 100644 index 000000000000..958c03223272 Binary files /dev/null and b/docs/images/projectportfolio.png differ diff --git a/docs/images/projectteam.png b/docs/images/projectteam.png new file mode 100644 index 000000000000..19fa63a73001 Binary files /dev/null and b/docs/images/projectteam.png differ diff --git a/docs/images/quoteend.png b/docs/images/quoteend.png new file mode 100644 index 000000000000..ac0888e0cdcd Binary files /dev/null and b/docs/images/quoteend.png differ diff --git a/docs/images/quotestart.png b/docs/images/quotestart.png new file mode 100644 index 000000000000..a5e1d2607dc6 Binary files /dev/null and b/docs/images/quotestart.png differ diff --git a/docs/images/screenshot.png b/docs/images/screenshot.png new file mode 100644 index 000000000000..32a4f97fdc48 Binary files /dev/null and b/docs/images/screenshot.png differ diff --git a/docs/images/settingup.png b/docs/images/settingup.png new file mode 100644 index 000000000000..2381c4e97ecf Binary files /dev/null and b/docs/images/settingup.png differ diff --git a/docs/images/testingheader.png b/docs/images/testingheader.png new file mode 100644 index 000000000000..840a9706a1d3 Binary files /dev/null and b/docs/images/testingheader.png differ diff --git a/docs/images/userguide.png b/docs/images/userguide.png new file mode 100644 index 000000000000..d4402f7fe3d5 Binary files /dev/null and b/docs/images/userguide.png differ diff --git a/docs/images/userguideexample.png b/docs/images/userguideexample.png new file mode 100644 index 000000000000..f4011ffb7516 Binary files /dev/null and b/docs/images/userguideexample.png differ diff --git a/docs/images/userguideheader.png b/docs/images/userguideheader.png new file mode 100644 index 000000000000..53d7c2d7e17c Binary files /dev/null and b/docs/images/userguideheader.png differ diff --git a/docs/images/wailunlim.png b/docs/images/wailunlim.png new file mode 100644 index 000000000000..5a4588c29152 Binary files /dev/null and b/docs/images/wailunlim.png differ diff --git a/docs/images/weddingcouple.png b/docs/images/weddingcouple.png new file mode 100644 index 000000000000..5bb0ab053ebc Binary files /dev/null and b/docs/images/weddingcouple.png differ diff --git a/docs/images/yijinl.jpg b/docs/images/yijinl.jpg deleted file mode 100644 index adbf62ad9406..000000000000 Binary files a/docs/images/yijinl.jpg and /dev/null differ diff --git a/docs/images/yl_coder.jpg b/docs/images/yl_coder.jpg deleted file mode 100644 index 17b48a732272..000000000000 Binary files a/docs/images/yl_coder.jpg and /dev/null differ diff --git a/docs/stylesheets/gh-pages.css b/docs/stylesheets/gh-pages.css index 121cac3885fd..d8c498b05372 100644 --- a/docs/stylesheets/gh-pages.css +++ b/docs/stylesheets/gh-pages.css @@ -1,4 +1,5 @@ @import url(https://fonts.googleapis.com/css?family=Montserrat|Open+Sans); +@import "style.css"; @import "asciidoctor.css"; /* Default asciidoc style framework - important */ /* Custom block: details */ diff --git a/docs/stylesheets/style.css b/docs/stylesheets/style.css new file mode 100644 index 000000000000..d2bbd65a9582 --- /dev/null +++ b/docs/stylesheets/style.css @@ -0,0 +1,5 @@ +@media print { + a[href]:after { + content: none !important; + } +} diff --git a/docs/team/dongsiji.adoc b/docs/team/dongsiji.adoc new file mode 100644 index 000000000000..62f5932f1779 --- /dev/null +++ b/docs/team/dongsiji.adoc @@ -0,0 +1,319 @@ += Dong SiJi - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets +:sectnums: + +[none] +== PROJECT: Heart² + +The purpose of this document is to document the contribution that I have made in the project: *_Heart²_* +as a student undertaking the module CS2103T under NUS School of Computing. + +*_Heart²_* github link: https://github.com/CS2103-AY1819S1-F10-3/main + +--- + +== Introduction + +*_Heart²_* is a desktop software aiming to make the job of wedding planning agencies simpler. +It provides simple yet powerful features to efficiently manage clients' and agency companies' profiles. +Users can find suitable wedding service providers for clients using just a few keystrokes with our enterprise feature set. + +The project is written in Java and has about 10 thousand lines of code. The user interacts with the application mainly using a Command Line Interface (CLI), +and it also has a Graphic User Interface (GUI) created with JavaFX for users who are not so adept with the keyboard. + +== Summary of contributions + +|=== +|Below is a summary of the contributions that I have made to the project. +|=== + +=== Major Enhancement +* *Major enhancement*: Revamp the `list` command. +* *Functionality*: The user is able to use the `list` command to list all clients or vendors. They are also able to use the command to search through clients and vendors by providing the relevant keywords that they want to search for. +* *Justification*: By having both the searching and listing functionality in one command the user will have less commands to familiarise with and it increases the ease of use of the application. +* *Highlights*: To revamp the `list` command, I need to have a deep understanding of how the commands work as well as how the parser works, which I spent time to familiarise myself with. I also had to change how some of the information was stored in the application, so that the relevant information could be retrieved easily when needed. +* *Functional Code Contributed*: link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/docs/team/dongsijiContributedCodeList.adoc[all code] +* *Test Code Contributed*: link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/150[all tests] + +=== Minor Enhancement +* *Minor enhancement*: Enhance `undo` and `redo` commands to display the list which was modified. +* *Functionality*: The list shown to the user after executing the `undo` and `redo` commands will be the list of clients or vendors that was changed through the command. +* *Justification*: This is to allow the user to be able to see what was changed at a glance without needing to navigate to the corresponding list. +* *Functional Code Contributed*: link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/188[all code] +* *Test Code Contributed*: link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/188[all tests] + +=== Other Contributions + +* *Community*: +** Reported bugs and provide suggestions to other team members' code. Check it out link:https://github.com/CS2103-AY1819S1-F10-3/main/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Atype.Bug+involves%3Adongsiji[here]. + +* *Tools*: +** Set up link:https://heart2.netlify.com/[Netlify]. + +* *Documentation*: +** Wrote the link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/docs/AboutUs.adoc[about us] documentation. +** Contributed to the User Guide and Developer Guide for this project. See below for more details. + +* *Summary of contributions*: +** Over 35 link:https://github.com/CS2103-AY1819S1-F10-3/main/pulls?page=1&q=is%3Apr+author%3Adongsiji&utf8=%E2%9C%93[Pull Requests] on Github. +** Over 25 link:https://github.com/CS2103-AY1819S1-F10-3/main/issues?utf8=%E2%9C%93&q=is%3Aissue+author%3Adongsiji+[Issues] raised on Github. +** Over 30 link:https://github.com/CS2103-AY1819S1-F10-3/main/pulls?utf8=%E2%9C%93&q=is%3Apr+commenter%3Adongsiji[Reviews] of Pull Requests on Github. +** Overview of all link:https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=dongsiji&sort=displayName&since=2018-09-12&until=2018-11-12&timeframe=day&reverse=false&repoSort=true[contributions]. + +== Contributions to the User Guide + +|=== +|Below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users. +|=== + +=== Listing and searching for contacts: `list` + +You can view all the contacts of a specific type in a list by specifying the contact type to be shown. + +Format: ` list` + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client list` +|===== + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor list` +|===== + +*_Heart²_* also supports searching via name, phone number, email, address and tags for you to quickly find your contacts. +To search, simply append your search parameters to the back of the original command. + +Only contacts that matches all the search parameters will be displayed to you in the form of a list. +Searching is done through substring matching, so you do not need to enter the full name, just part of the name will do. + +Format: ` list [n/FULL_NAME] [p/PHONE_NUMBER] [e/EMAIL_ADDRESS] [a/HOME_ADDRESS] [t/TAG]...` + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client list n/John` +|===== + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor list p/98765432` +|===== + +=== Undo the previous command: `undo` + +Undo the commands that you have entered in chronological order for this particular session. Once you logout or exit the application, you cannot `undo` a command from the last session. + +Format: `undo` + +[NOTE] +The application will only undo commands that modifies the list of contacts: `add`, `update`, `delete`, `clear` + +[NOTE] +The application will show either the client list or vendor list corresponding to the command that was undone. + +=== Redo the commands undone: `redo` + +Redo the commands that you have undone by undo in chronological order for this particular session. Once you logout or exit the application, you cannot `redo` a command from the last session. + +Format: `redo` + +[NOTE] +Commands that have been undone will be reset upon a `clear` command. + +[NOTE] +The application will show either the client list or vendor list corresponding to the command that was redone. + +=== Command summary + +Below is a summary of the commands available for you to use. + +*Before logging in* + +[cols="^30,^70", options="header"] +|=== +|FEATURE |FORMAT +|To get help | `help` +|To log in | `login u/USERNAME p/PASSWORD` +|To close the application | `exit` +|=== + +*After logging in* + +[cols="^30,^70", options="header"] +|=== +|FEATURE |FORMAT +|To register a new account | `register account u/USERNAME p/PASSWORD r/ROLE` +|To change your account password | `change password o/YOUR_OLD_PASSWORD n/YOUR_NEW_PASSWORD` +|To add a contact | ` add n/FULL_NAME p/PHONE_NUMBER e/EMAIL_ADDRESS a/HOME_ADDRESS [t/TAG]…​` +|To add a service requirement | `# addservice s/SERVICE_TYPE c/SERVICE_COST` +|To update a specific contact | `# update [n/FULL_NAME] [p/PHONE_NUMBER] [e/EMAIL_ADDRESS] [a/HOME_ADDRESS] [t/TAG]...` +|To list contacts that matches the inputs | ` list [n/FULL_NAME] [p/PHONE_NUMBER] [e/EMAIL_ADDRESS] [a/HOME_ADDRESS] [t/TAG]...` +|To find a match that fits a particular contact's requirements | `# automatch` +|To view a specific contact | `# view` +|To delete a specific contact | `# delete` +|To delete all contacts | `clear` +|To list all the commands entered in this session | `history` +|To undo the previous command | `undo` +|To redo the previous undone command | `redo` +|To get help | `help` +|To log out of your account | `logout` +|To close the application | `exit` +|=== + +== Contributions to the Developer Guide + +|=== +|Below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project. +|=== + +=== List Feature +*_Heart²_* allows you view all the clients or the vendors with a simple command: `list`. + +When listing contacts, you would have to specify whether the contact is a client or a vendor +by prefixing it to list: + +* `client list` +* `vendor list` + +Below shows an example of how listing all clients works: + +._The UI showing how to list all clients._ +image::ListAllClients.png[width="800"] + +Furthermore, you are also able to add keywords after the list to do filtering, and each keyword is specified to +belong to a category and only contacts which contains all of the keywords in their respective categories will be shown. + +[NOTE] +==== +Categories include: + +* `n/ NAME` +* `p/ PHONE_NUMBER` +* `e/ EMAIL_ADDRESS` +* `a/ ADDRESS` +* `t/ TAGS` +==== + +Below shows an example of how list filtering works: + +._The UI showing list filtering._ +image::ListClientsWithKeywords.png[width="800"] + +==== Implementation + +The keywords from the command to be used for filtering is parsed by the `ListCommandParser` into a `ContactInformation` +and passed to a `Predicate` to be used for filtering. The `Predicate` is implemented as `ContactContainsKeywordsPredicate`. + +Below is a sequence diagram showing the creation of the `ListCommand`. + +._The Sequence Diagram of the creation of a list command._ +image::ListSequenceDiagram1.png[width="800"] + +We use a `FilteredList` and pass the combination of 2 `Predicates` into it, one to filter the type of contact, +clients or vendors and the other is to filter by keywords, which is the `ContactContainsKeywordsPredicate` from the `ListCommandParser`. + +Below is a sequence diagram showing the execution of the `ListCommand`. + +._The Sequence Diagram of the execution of a list command._ +image::ListSequenceDiagram2.png[width="800"] + +==== Design considerations + +[none] +==== Aspect 1: Substring Matching or Word Matching +* *Alternative 1 (current choice):* Substring matching. +** Pros: Users would be able to view a wider range of results that matches the substring they have given. Easier to use. +** Cons: Irrelevant results might not be filtered away if they contain the substring. +* *Alternative 2:* Word matching. +** Pros: Guarantees that no irrelevant results are shown. +** Cons: Relevant results that have a small difference in the wording will be filtered away and not shown. + +[none] +==== Aspect 2: Categorised or Non-categorised keywords +* *Alternative 1 (current choice):* Categorised keywords. +** Pros: Users are able to specify which keywords they want to search for in which category. +Gives better control over the searching. +** Cons: Users have to follow a specific format to type the keywords. +* *Alternative 2:* Non-categorised keywords. +** Pros: User can type in the keywords in any order they want. Easier to use. +** Cons: Irrelevant results that contains the keywords will be shown. + +[none] +==== Aspect 3: All Match or Any Match +* *Alternative 1 (current choice):* All match. +** Pros: Users can specify what they want to search for and filter out all irrelevant results. +** Cons: Users are not able to search for multiple things, when they only require one of them to match. +* *Alternative 2:* Any match. +** Pros: Users are able to obtain a wider search result. Easier to use. +** Cons: Irrelevant results that contains only one or a few keywords will be shown as well. + +=== Undo and Redo Aspect: What it shows after undo/redo command successfully executes + +* *Alternative 1 (current choice):* Shows the list that was changed due to the undo/redo command. +** Pros: Easy for the user to identify what was changed, whether a client or vendor was modified. +** Cons: It switches the list out of the current filter and the user have to re-type the list command if he wants to filter the list. +* *Alternative 2:* Keeps showing what was shown before the command was executed. +** Pros: Easy to implement. +** Cons: Hard for the user to identify what was changed in the addressbook. +* *Alternative 3:* Show what was changed, before and after. +** Pros: User can easily tell what was changed. +** Cons: Hard to implement, need to have an additional UI components to show what was changed and need additional components to store the list before it was changed. + +=== Use Cases + +==== Use case: List all the Clients or Vendors + +*Preconditions*: User is logged in with a `SUPER_USER` account. + +*MSS* + +1. User enters the list command and requests to view either all the Clients, or all the Vendors. +2. System returns either a list with all the Clients' information, or all the Vendors' information. ++ +Use case ends. + +*Extensions* + +[none] +* 2a. There is no Client or no Vendor available ++ +[none] +** 2a1. System returns an empty list. + ++ +Use case ends. + +==== Use case: Filter and show Client’s or Vendor’s info according to the filter + +*Preconditions*: User is logged in with a `SUPER_USER` account. + +*MSS* + +1. User enters the list command and requests to view either Client’s or Vendor’s information with some +keywords provided indicated by prefixes. +2. The System displays a list of Clients or Vendors whose information matches what was provided. ++ +Use case ends. + +*Extensions* + +[none] +* 1a. User enters a prefix that does not exist. ++ +[none] +** 1a1. System prompts the User the correct format of the command and prefixes that can be used. + +* 1b. User enters an empty prefix. ++ +[none] +** 1b1. System prompts the User the correct format of the command and prefixes that can be used. + ++ +Use case ends. diff --git a/docs/team/dongsijiContributedCodeList.adoc b/docs/team/dongsijiContributedCodeList.adoc new file mode 100644 index 000000000000..8b9ced1e8ddf --- /dev/null +++ b/docs/team/dongsijiContributedCodeList.adoc @@ -0,0 +1,19 @@ += Contributed Code For List Command + +:site-section: AboutUs +:stylesDir: ../stylesheets + +|=== +| Below is a collection of all the functional code that I have contributed for the list feature. +|=== + +* link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/47[find function] +* link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/59[add list functionality into find] +* link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/71[delete list command] +* link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/91[refactor find to list] +* link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/129[change any match to all match] +* link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/136[update parser for list] +* link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/141[remove EntryContainsKeywordsPredicate class] +* link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/142[make list case insensitive] +* link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/171[fix null pointer from empty inputs] +* link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/241[fix identifier not caught as error for list command] diff --git a/docs/team/eehooi.adoc b/docs/team/eehooi.adoc new file mode 100644 index 000000000000..31cfda1e6317 --- /dev/null +++ b/docs/team/eehooi.adoc @@ -0,0 +1,139 @@ += Ng Ee Hooi - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: Heart² + +--- +This document serves to note down my contributions to https://github.com/CS2103-AY1819S1-F10-3/main[*_Heart²_*], my team's product for our +Software Engineering project. + +== Overview + +*_Heart²_* is a desktop software meant for wedding planning agencies to efficiently manage clients' and agency companies' profiles. +It is primarily used for matching various wedding vendors to clients' specific needs. + +The user keys in commands using a _Command Line Interface (CLI)_, and views the command results on a _Graphical User Interface (GUI)_ created with JavaFX. +*_Heart²_* was built upon a given https://github.com/se-edu/addressbook-level4[base code] provided by the SE-EDU team. +It is written in Java, and has about 20 kLoC. + +*_Heart²_* also supports both Windows and Mac operating systems. + +== Summary of Contributions + +This section provides the overall view of my contributions to *_Heart²_*. +All my pull requests (PRs) can be found https://github.com/CS2103-AY1819S1-F10-3/main/pulls?q=is%3Apr+author%3Aeehooi[here]. + +=== Major Enhancements + +I have contributed 2 supportive features for *_Heart²_*'s main feature `AutoMatch`. +`AutoMatch` finds all vendors that fulfill the specified client's requirements or clients that the specified vendor can serve. + +1. *Ability to add a `Service` under a client and vendor* +** *What it does:* Allows the user to specify a service type and cost requested by the client or offered by the vendor. +** *Justification:* This feature is a crucial first step for `AutoMatch` as it provides the criteria for +filtering out the matching contacts for a particular contact enquired. +** *Code contributed:* Activation [https://github.com/CS2103-AY1819S1-F10-3/main/pull/174[#174]], +Enhancement [https://github.com/CS2103-AY1819S1-F10-3/main/pull/202[#202]], +`Service` Cost BigDecimal Integration [https://github.com/CS2103-AY1819S1-F10-3/main/pull/229[#229], +https://github.com/CS2103-AY1819S1-F10-3/main/pull/237[#237]], +Test [https://github.com/CS2103-AY1819S1-F10-3/main/pull/280[#280], https://github.com/CS2103-AY1819S1-F10-3/main/pull/284[#284]] + +2. *Viewing the results of `AutoMatch` in a tabular view* +** *What it does:* Shows the user the vendors that fulfill the client requirements or the clients that the vendors can serve in a single table. +** *Justification:* This feature categorises the results of `AutoMatch` based on each service type. +Instead of returning users a single string of vendor names, displaying the results in a single table with the relevant information would enable a much easier selection. +** *Highlights:* I had to pick up a new XML-based user interface markup language, FXML, from scratch in order to incorporate this new `UiPart`. +Furthermore, this enhancement required the integration of 3 major components of *_Heart²_*: `Logic`, `Model` and `Ui` in order to populate the table with the right information. +** *Code contributed:* Design [https://github.com/CS2103-AY1819S1-F10-3/main/pull/162[#162]], Integration [https://github.com/CS2103-AY1819S1-F10-3/main/pull/206[#206]] + +=== Minor Enhancement +1. *Login window UI display before login and after logout* +** *What it does:* Prevents the user from accessing the `MainWindow` before login and after logout. +** *Justification:* This enhancement complements the `Login` and `Logout` feature. +With a separate login window, it would be clear to users that they would have to login in order to access the data. +This prevents confusion as to why the data is not shown on the main screen. +** *Highlights:* This enhancement disrupts the original flow of logic of displaying the `MainWindow` right when *_Heart²_* is launched. +I had to create a new `loginStage` and activate it based on the respective `Event` raised, whilst hiding the `primaryStage`. +Detailed understanding of how `Event` and event `Subscribers` worked was also critical. +** *Code contributed:* Design [https://github.com/CS2103-AY1819S1-F10-3/main/pull/66[#66], https://github.com/CS2103-AY1819S1-F10-3/main/pull/101[#101]], +Activation [https://github.com/CS2103-AY1819S1-F10-3/main/pull/148[#148], https://github.com/CS2103-AY1819S1-F10-3/main/pull/154[#154]], +Enhancement [https://github.com/CS2103-AY1819S1-F10-3/main/pull/163[#163], https://github.com/CS2103-AY1819S1-F10-3/main/pull/207[#207], +https://github.com/CS2103-AY1819S1-F10-3/main/pull/231[#231], https://github.com/CS2103-AY1819S1-F10-3/main/pull/276[#276]], +Logout Integration [https://github.com/CS2103-AY1819S1-F10-3/main/pull/189[#189]] + + +=== Other Contributions +** *Project Management* +*** Did minor refactoring to make the code more comprehensive and uniform. [https://github.com/CS2103-AY1819S1-F10-3/main/pull/280[#280]] +*** Updated App name to *_Heart²_*. [https://github.com/CS2103-AY1819S1-F10-3/main/pull/111[#111]] +*** Removed unnecessary images. [https://github.com/CS2103-AY1819S1-F10-3/main/pull/103[#103]] +** *Documentation* +*** Changed GUI images for login and main window. [https://github.com/CS2103-AY1819S1-F10-3/main/pull/211[#211], https://github.com/CS2103-AY1819S1-F10-3/main/pull/215[#215], https://github.com/CS2103-AY1819S1-F10-3/main/pull/285[#285]] +*** Updated Ui class diagram to incorporate new `LoginWindow` and `ServiceListPanel`. +*** Added and updated introductions to the User Guide and Developer Guide. [https://github.com/CS2103-AY1819S1-F10-3/main/pull/55[#55], https://github.com/CS2103-AY1819S1-F10-3/main/pull/56[#56]] +*** Added design considerations for `LoginCommand` and `AutoMatchCommand`. [https://github.com/CS2103-AY1819S1-F10-3/main/pull/110[#110]] +*** Added implementation details for `AddServiceCommand` [https://github.com/CS2103-AY1819S1-F10-3/main/pull/185[#185], https://github.com/CS2103-AY1819S1-F10-3/main/pull/323[#323]] +*** Updated roles in AboutUs. [https://github.com/CS2103-AY1819S1-F10-3/main/pull/285[#285]] +** *Minor UI Contributions* +*** Update tag color to fit in with *_Heart²_*'s theme [https://github.com/CS2103-AY1819S1-F10-3/main/pull/44[#44]] +*** Generate URL string of `Contact` details for `BrowserPanel` [https://github.com/CS2103-AY1819S1-F10-3/main/pull/208[#208]] +*** Updated *_Heart²_*'s launch logo. [https://github.com/CS2103-AY1819S1-F10-3/main/pull/296[#296]] +** *Create Sample Data for `.jar` launch* [https://github.com/CS2103-AY1819S1-F10-3/main/pull/301[#301]] +** *Community* +*** Actively looked through and approved PRs. +*** Actively raised issues in the issue tracker. +*** Always ensured that at least one team member has approved my PR before merging. +** *Tools* +*** Set up Coveralls. [https://github.com/CS2103-AY1819S1-F10-3/main/pull/38[#38]] +** *Overall Code Contributed:* https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=eehooi[RepoSense] + +== Contributions to the User Guide + +|=== +|_This section includes excerpts of the User Guide that I have contributed to._ +|=== + +=== Excerpt: Add Service Feature + +--- + +include::../UserGuide.adoc[tag=addservice] + + +== Contributions to the Developer Guide + +|=== +|_This section includes excerpts of the Developer Guide that I have contributed to._ +|=== + +=== Excerpt 1: Login UI Design Considerations + +--- + +include::../DeveloperGuide.adoc[tag=loginui] + +=== Excerpt 2: Add Service Feature + +--- + +include::../DeveloperGuide.adoc[tag=addservice] + +=== Excerpt 3: Automatch UI Design Considerations + +--- + +include::../DeveloperGuide.adoc[tag=automatchui] + +=== Excerpt 4: Add Service Use Case + +--- + +include::../DeveloperGuide.adoc[tag=addserviceuc] + +=== Excerpt 5: Add Service Manual Testing + +--- + +include::../DeveloperGuide.adoc[tag=addservicemanual] diff --git a/docs/team/johndoe.adoc b/docs/team/johndoe.adoc deleted file mode 100644 index 453c2152ab9d..000000000000 --- a/docs/team/johndoe.adoc +++ /dev/null @@ -1,72 +0,0 @@ -= John Doe - Project Portfolio -:site-section: AboutUs -:imagesDir: ../images -:stylesDir: ../stylesheets - -== PROJECT: AddressBook - Level 4 - ---- - -== Overview - -AddressBook - Level 4 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. - -== Summary of contributions - -* *Major enhancement*: added *the ability to undo/redo previous commands* -** What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. -** Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. -** Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. -** Credits: _{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}_ - -* *Minor enhancement*: added a history command that allows the user to navigate to previous commands using up/down keys. - -* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ - -* *Other contributions*: - -** Project management: -*** Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub -** Enhancements to existing features: -*** Updated the GUI color scheme (Pull requests https://github.com[#33], https://github.com[#34]) -*** Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests https://github.com[#36], https://github.com[#38]) -** Documentation: -*** Did cosmetic tweaks to existing contents of the User Guide: https://github.com[#14] -** Community: -*** PRs reviewed (with non-trivial review comments): https://github.com[#12], https://github.com[#32], https://github.com[#19], https://github.com[#42] -*** Contributed to forum discussions (examples: https://github.com[1], https://github.com[2], https://github.com[3], https://github.com[4]) -*** Reported bugs and suggestions for other teams in the class (examples: https://github.com[1], https://github.com[2], https://github.com[3]) -*** Some parts of the history feature I added was adopted by several other class mates (https://github.com[1], https://github.com[2]) -** Tools: -*** Integrated a third party library (Natty) to the project (https://github.com[#42]) -*** Integrated a new Github plugin (CircleCI) to the team repo - -_{you can add/remove categories in the list above}_ - -== Contributions to the User Guide - - -|=== -|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ -|=== - -include::../UserGuide.adoc[tag=undoredo] - -include::../UserGuide.adoc[tag=dataencryption] - -== Contributions to the Developer Guide - -|=== -|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ -|=== - -include::../DeveloperGuide.adoc[tag=undoredo] - -include::../DeveloperGuide.adoc[tag=dataencryption] - - -== PROJECT: PowerPointLabs - ---- - -_{Optionally, you may include other projects in your portfolio.}_ diff --git a/docs/team/liaujianjie.adoc b/docs/team/liaujianjie.adoc new file mode 100644 index 000000000000..21df3198d3ab --- /dev/null +++ b/docs/team/liaujianjie.adoc @@ -0,0 +1,211 @@ += Liau Jian Jie - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== Introduction + +This document serves as a summary for my contribution to the https://github.com/CS2103-AY1819S1-F10-3/main[*Heart²* +project]. + +For more, you can visit my https://jianjie.co[personal website] to see other projects I was involved in. + +== Overview + +*Heart²* is a cross-platform enterprise wedding management desktop application that enables wedding agencies to easily +match clients (couples looking to have a wedding ceremony) to vendors (companies that offer services catered to +weddings). + +*Some features include*: + +* CLI interface and rich GUI interface +* Client and vendor database +* Detailed tabular breakdown of services listed +* Auto-matching of clients and vendors + +*General metrics of my contribution* (as of 4 Nov 2018) + +* ✔ 2,136 LoC changed +* ✔ 38 commits created +* ✔ 26 pull requests merged +* ✔ 19 issues raised +* Code contribution on https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=liaujianjie[reposense] + +== Summary of contributions + +=== *Major enhancement*: Auto-matching +** *What it does:* Automatically finds vendors that can fulfill the service requirements of a specific client. +Inversely, it can also find all clients for which a specific vendor can serve. +** *Justification:* Manually searching for vendors for clients is extremely laborious and repetitive. This feature +reduces the search of vendors or clients to a single line of command. +** *Highlights:* Simple single one-line command: `client#123 automatch` +** *Contribution to functional code:* https://github.com/CS2103-AY1819S1-F10-3/main/pull/88[#88], https://github.com/CS2103-AY1819S1-F10-3/main/pull/195[#195], https://github.com/CS2103-AY1819S1-F10-3/main/pull/199[#199], https://github.com/CS2103-AY1819S1-F10-3/main/pull/205[#205], https://github.com/CS2103-AY1819S1-F10-3/main/pull/222[#222], https://github.com/CS2103-AY1819S1-F10-3/main/pull/240[#240], https://github.com/CS2103-AY1819S1-F10-3/main/pull/274[#274], https://github.com/CS2103-AY1819S1-F10-3/main/pull/277[#277] +** *Contribution to tests:* https://github.com/CS2103-AY1819S1-F10-3/main/pull/291[#291] + +=== *Major enhancement*: Profile panel +** *What it does:* Renders the profile panel in an aesthetically appealing visual format. +** *Justification:* The previous static page was not very helpful and could not be customised to contain objects such as +arrays or maps. +** *Highlights:* Built with the blazing-fast GatsbyJS React Javascript framework for static websites. +** *Note:* The code for this portion resides in a https://github.com/CS2103-AY1819S1-F10-3/profile-site[separate +repository]. + + +image::automatching.png[width:600] +_Annotated screenshot of the auto-matching feature and profile panel_ + +=== *Minor enhancement*: Contact type serialization +** *What it does:* Serializes the contact (both clients and vendors) as different types of entities in the same XML +file. +** *Justification:* Prior to this change, there was no mechanism to differentiate between clients and vendors when the +data was de-serialized from the XML file. +** *Highlights:* Serializes both client and vendor data into the same address book XML each type with a differentiable +format. +** *Contribution to functional code:* https://github.com/CS2103-AY1819S1-F10-3/main/pull/143[#143] + +=== *Other contributions*: +** Refactored `ServiceProvider` to `Vendor` (https://github.com/CS2103-AY1819S1-F10-3/main/pull/239[#239]) +** Ensure there is only one service name and cost when adding service (https://github.com/CS2103-AY1819S1-F10-3/main/pull/273[#273]) +** Made sure repository was up to date with the upstream branch (https://github.com/CS2103-AY1819S1-F10-3/main/pull/79[#79]) +** Minor readme updates (https://github.com/CS2103-AY1819S1-F10-3/main/pull/35[#35], https://github.com/CS2103-AY1819S1-F10-3/main/pull/49[#49]) +** Took initiative to ensure that we had everything prioritized in the right order and ensure the team was focused +during our discussions. +** Spearheaded the various architecture design discussions. + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting +end-users._ +|=== + +=== Modifying the global structure of user guide + +Since I was involved in the modelling of our application, I took charge in making the first wave of sweeping updates to our user guide. In doing so, I created a clear separation between _entity types_ (such as `client`) and _entity instances_ (like `client#123`). + +Commands acting on a collection of a specific entity type were preceded by the name of the entity type whilst commands acting on a specific entity instance are preceded by a composition of the instance entity type and identifier. + +Example documentation of a command that acts on an entity type: + +==== +*Listing and searching for contacts: `list`* + +You can view all the contacts of a specific type in a list by specifying the contact type to be shown. + +Format: ` list` + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client list` +|===== + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor list` +|===== +==== + +Example documentation of a command that acts on an entity instance: + +==== +*Deleting a contact: `delete`* + +You can also `delete` a contact from *_Heart²_*, by specifying its unique ID. + +Format: `# delete` + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client#123 delete` +|===== +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor#123 delete` +|===== + +[TIP] +You can `undo` and `redo` deleting a contact! +==== + +Pull requests: https://github.com/CS2103-AY1819S1-F10-3/main/pull/30[#30], https://github.com/CS2103-AY1819S1-F10-3/main/pull/64[#64] + +=== Documentation for auto-matching + +I wrote the entire instructions for using one of the most critial feature of our application--auto-matching. + +==== +*Automatching for a client: `automatch`* + +You can easily find vendors that can fulfil the request services with this command. + +Format: `client# automatch` + +[NOTE] +It only shows you the vendors within the budget requirement that fulfils a particular service requirement of the client. + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client#123 automatch` +|===== +==== + +You can see it https://cs2103-ay1819s1-f10-3.github.io/main/UserGuide.html#automatching-for-a-client-code-automatch-code[in the user guide]. + +Pull request: https://github.com/CS2103-AY1819S1-F10-3/main/pull/210[#210] + +=== Experimenting with icons + +Additionally, I have experimented with coloured icons for notes, tips and warnings to provide a consistent visual format when additional content is appended. We eventually decided to remove it because of formatting issues with AsciiDoctor. + +==== +image:icon-notes.png[width="48"] +image:icon-tips.png[width="48"] +image:icon-danger.png[width="48"] +==== + +Pull request: https://github.com/CS2103-AY1819S1-F10-3/main/pull/290[#290] + +=== Other changes + +I have also made some minor tweaks to make the user guide squeaky clean. + +Pull requests: https://github.com/CS2103-AY1819S1-F10-3/main/pull/213[#213], https://github.com/CS2103-AY1819S1-F10-3/main/pull/272[#272], https://github.com/CS2103-AY1819S1-F10-3/main/pull/279[#279], https://github.com/CS2103-AY1819S1-F10-3/main/pull/289[#289] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical +documentation and the technical depth of my contributions to the project._ +|=== + +=== Document design decisions for auto-matching + +I documented the design decisions behind auto-matching and created a custom graphic to assist in the explanation. Below is an excerpt from the developer guide: + +==== +*Finding matches between clients and service providers* + +The application boasts auto-matching features that reduces the (once-laborious) task of matching service providers a single command. + +*High level design* + +.High level overview of how auto-matching works +image::auto-matching.png[width:"800"] + +1. On invocation, the auto-matching algorithm functionally maps all service requirements from a Client into predicates for performing the first step of filtering the Service Providers. +2. The service providers are then sorted by a fair ranking algorithm to ensure even distribution of jobs between Service Providers. +==== + +To see more, check it out https://cs2103-ay1819s1-f10-3.github.io/main/DeveloperGuide.html#finding-matches-between-clients-and-service-providers[in the developer guide]. + +Pull request: https://github.com/CS2103-AY1819S1-F10-3/main/pull/117[#117] + +''' + +To see more of my works, visit my https://jianjie.co[personal website] to see other projects I worked on. diff --git a/docs/team/nightyeti.adoc b/docs/team/nightyeti.adoc new file mode 100644 index 000000000000..1f304bf1a129 --- /dev/null +++ b/docs/team/nightyeti.adoc @@ -0,0 +1,352 @@ += Gan Chin Yao - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:sectnums: +:stylesDir: ../stylesheets + +image::projectportfolio.png[width="600"] + +This portfolio aims to provide a summary of what I have contributed as a student for the project *_Heart²_*. + +*_Heart²_* Github link: https://github.com/CS2103-AY1819S1-F10-3/main + +== Introduction +image::abouttheproject.png[width="400"] + +image::intropicture.png[width="200"] + +*_Heart²_* is a desktop application for wedding planning agencies to efficiently manage their clients and vendors. It allows the agency to automatically match the agency's vendors to existing clients' needs, and also maintains a easy-to-use database search system for clients' and vendors' profiles. + +It is written in Java, and has about 10 kLoC. The user interacts with it using a Command Line Interface (CLI), and it has a Graphical User Interface (GUI) created with JavaFX + +It is cross-platform and can be compiled for both Windows and Mac OS. + + +image::projectteam.png[width="400"] + +*_Heart²_* was developed on top of the link:https://github.com/nus-cs2103-AY1819S1/addressbook-level4[AddressBook - Level 4] application. It consists of 5 dedicated members, including myself. I was the Team Lead in *_Heart²_*, and doubled up as a software engineer. I coded a `Login` feature from scratch and `Privileges` system for different types of user accounts. + +{empty} + + +== Summary of contributions + +|=== +|_This section provides an overview of the contributions I have done for this application._ +|=== + +image::major.png[width="400"] + +=== Major enhancements + +Added `login` and `logout` functions, and allow users to `register` for an user account. + +* *What it does:* Allows the user to register for an account, and use that account to log in to the application. Afterwards, the user can log out of his/her account. +* *Justification:* Clients' and vendors' personal information are present in the application, and it is necessary to protect these information from unwanted eyes. Logging in to use the application is an important feature as it restricts access to the application from unauthorised parties. Without an authorised account, the user is unable to use this application as it is mandatory to log in at start. +* *Highlights:* Storing user account information was a challenge as the project restriction only allows files to be stored locally. Nonetheless, I stored user password as hash digest using `PBKDF2WithHmacSHA512` algorithm with salt to prevent anyone from looking up the user's password directly. The implementation is based off this link:http://stackoverflow.com/a/2861125/3474[stackoverflow] answer. +* *Functional code contributed*: link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/97[Register account] | link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/135[Login] | link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/main/java/seedu/address/logic/commands/LogoutCommand.java[Logout] | link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/173[Salt and Hash] | link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/main/java/seedu/address/storage/XmlAdaptedAccount.java[Storage] | link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/main/java/seedu/address/storage/XmlAccountStorage.java[Storage 2] | link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/main/java/seedu/address/storage/XmlSerializableAccountList.java[Storage 3] +* *Test code contributed*: link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/test/java/seedu/address/logic/commands/RegisterAccountCommandTest.java[Register account] | link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/test/java/seedu/address/logic/commands/LoginCommandTest.java[Login] | link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/test/java/seedu/address/logic/commands/LogoutCommandTest.java[Logout] | link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/test/java/seedu/address/logic/security/PasswordAuthenticationTest.java[Hash] | link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/test/java/seedu/address/storage/XmlAdaptedAccountTest.java[Storage] | link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/test/java/seedu/address/storage/XmlAccountStorageTest.java[Storage 2] | link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/test/java/seedu/address/storage/XmlSerializableAccountListTest.java[Storage 3] + + +image::minor.png[width="400"] + +=== Minor enhancements + +* *Minor enhancement:* Allows user to `edit password` as well as creating account access privilege with either `Super User` or `Read-Only User` role. +* *Justification:* Editing password is needed as user may want to change his/her password from time to time, especially for compromised accounts. Allocating account privileges helps in preventing unnecessary modifications to the application database. For example, an employee tasked as call-support role would be allocated `Read-Only User` role as he/she does not need to modify data in the application. +* *Functional Code contributed*: link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/165/[EditPassword] | link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/89[Access privilege] +* *Test Code contributed*: link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/test/java/seedu/address/logic/commands/EditPasswordCommandTest.java[Edit password] | link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/src/test/java/seedu/address/model/account/AccountTest.java[Access privilege] + +image::others.png[width="400"] + +=== Other contributions +* *Project Management*: +** Managed releases link:https://github.com/CS2103-AY1819S1-F10-3/main/releases/tag/v1.1[v1.1] | link:https://github.com/CS2103-AY1819S1-F10-3/main/releases/tag/v1.2[v1.2] | link:https://github.com/CS2103-AY1819S1-F10-3/main/releases/tag/v1.2.1[v1.2.1] | link:https://github.com/CS2103-AY1819S1-F10-3/main/releases/tag/v1.3[v1.3] on GitHub + +* *Graphic assets*: +** Added graphical content to make the existing User Guide and Developer Guide more pleasant to read: Pull requests link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/138[#138] | link:https://github.com/CS2103-AY1819S1-F10-3/main/pull/145[#145] +** Created the application logo image:logotransparentbackground.png[width="150"] +** Contributed to the User Guide and Developer Guide for this application. See Section 3 below for more details. + +* *Community*: +** Contributed 40+ link:https://github.com/CS2103-AY1819S1-F10-3/main/pulls?q=is%3Apr+is%3Aclosed+author%3ANightYeti[pull requests] on Github. +** Reviewed 50+ link:https://github.com/CS2103-AY1819S1-F10-3/main/issues?q=reviewed-by%3ANightYeti[pull requests] by other authors on Github. + +* *Presentation*: +** Gave 2 product demos of the application to the entire class, the first targeting general audiences, and the second targeting company's higher management. + +* *All code contributed*: +** Check out link:https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=nightyeti[here] + +{empty} + + +== Contributions to the User Guide + +image::userguideheader.png[width="400"] +|=== +|_This section shows what I have contributed to the User Guide. It showcases my ability to write documentation targetting end-user._ +|=== + +Link to the full User Guide: link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/docs/UserGuide.adoc[Github link] + +=== Overview of contributions +Below is an overview of what I have contributed towards the User Guide. + +==== Made the User Guide pretty +I created various graphic assets for the User Guide to make the User Guide more pleasing to read. Below are examples of some of the graphic assets I have created. + +.Examples of graphics contributed +image::userguideexample.png[width="500"] + +The left image above shows the overall title graphic that is present on the first page of the User Guide, and the right image shows an example of a title header to better differentiate each sections in the User Guide. This showcases my ability to create graphic assets from scratch. + + +==== Crafted an `example` format: + +I crafted a format which was used consistently by my team throughout the entire User Guide to let users easily identify an example of a real command. + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|* `client#1 addservice t/photography p/2000` + * `client#1 addservice t/catering p/10000` +|===== + +The above is the `example format` used consistently throughout the User Guide. User can simply copy `client#1 addservice t/photography p/2000` as a command and run it in the application. This allows users to copy a working command conveniently and paste it into the application to trigger the command. + +==== Wrote content for the User Guide +I have written paragraphs relating to the new commands I have added to the application to teach users how to use those commands. The content I have written for the User Guide is shown below in Section 3.2. + +=== Extract of User Guide written + +image::extract.png[width="400"] + +|=== +|_The following paragraphs show partially the extract I have written for the User Guide. Below are two different portions taken from the same User Guide, labelled as part `A` and part `B` in this portfolio._ +|=== + +image::quotestart.png[width="180"] + +==== *_Part A_* +image::parta.png[width="200"] + +[Discrete] +=== Logging in : `login` + +Securely logs you in to access the system with a registered account. By default, a root account with `SUPER_USER` privilege is provided, using the username `rootUser` and password `rootPassword`. + +Format: `login u/USERNAME p/PASSWORD` + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`login u/rootUser p/rootPassword` +|===== + +[Discrete] +=== Logging out : `logout` + +Securely logs you out of the system. + +Format: `logout` + +[Discrete] +=== Registering a new account : `register account` + +Register a new account for this application. You can only register a new account after logging in with a `SUPER_USER` account. By default, `rootUser` is a default account with `SUPER_USER` privilege. + +Format: `register account u/USERNAME p/PASSWORD r/ROLE` + +[NOTE] +`r/ROLE`: either `r/superuser` or `r/readonlyuser` to create a `SUPER_USER` account or `READ_ONLY_USER` account respectively. + +[NOTE] +It may sound counter-intuitive to require an account before registering a new account. We make this requirement as only authorised personal should be given an account. Ideally, the owner of the application should dictate the account given to employees by helping them register an account. + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`register account u/myNewUsername p/p@ssw0rd r/superuser` +|===== + +[Discrete] +=== Change the current password : `change password` + +Change your current account password from an old password to a new password. + +Format: `change password o/YOUR_OLD_PASSWORD n/YOUR_NEW_PASSWORD` + +[NOTE] +You cannot `undo` changing of password. + +[WARNING] +New password should not be the same as old password, and it cannot be empty, or contain spaces. + +[WARNING] +Make sure your password is typed correctly. There is no confirmation prompt once you execute the command. + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`change password o/jf3nv n/j9y3nd` +|===== + + +{empty} + + +[Discrete] +==== 3.2.2. *_Part B_* +image::partb.png[width="200"] + +[Discrete] +=== FAQ +*[red]#Q*#: [red]#What platform is this application available on?# + +*A*: This application is cross-platform, and can be used on both Windows and Mac OS. + +*[red]#Q*#: [red]#Is this application free-of-charge?# + +*A*: Yes, this application is open-source and can be use for free, even commercially. + +*[red]#Q*#: [red]#How can I report an issue?# + +*A*: You can raise an issue in the issue section and our team will get back to you as soon as possible. + +image::quoteend.png[width="180"] + +== Contributions to the Developer Guide + +image::developerguideheader.png[width="400"] + +|=== +|_This section shows what I have contributed to the Developer Guide. It showcases my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +Link to the full Developer Guide: link:https://github.com/CS2103-AY1819S1-F10-3/main/blob/master/docs/DeveloperGuide.adoc[Github link] + +=== Overview of contributions +Below in an overview of what I have contributed towards the Developer Guide. + +==== Made the Developer Guide pretty +I created graphic assets for the Developer Guide to draw the reader's attention. This also makes the Developer Guide more aesthetically pleasing. + +.Examples of graphics contributed +image::developerguideexample.png[width="450"] + +The image on the right shown above is an example of a title header I have created to better differentiate each sections. This showcases my ability to create graphic assets from scratch. + +==== Wrote content for the Developer Guide +I have written paragraphs relating to the implementation details, created diagrams, and documented use cases for the Developer Guide. The content I have written is shown below in Section 4.2. + +=== Extract of Developer Guide written +image::extract.png[width="400"] + +|=== +|_The following paragraphs show partially the extract I have written for the Developer Guide. Below are three different portions taken from the same Developer Guide, labelled as part `A`, part `B`, and part `C` in this portfolio._ +|=== + +image::quotestart.png[width="180"] + +==== *_Part A_* +image::parta.png[width="200"] + +[Discrete] +=== Account Storage component + +.Structure of the Account Storage Component +image::AccountStorageClassDiagram.png[width="700"] + + +*API* : link:{repoURL}/src/main/java/seedu/address/storage/AccountStorage.java[`AccountStorage.java`] + +The `AccountStorage` component + +* can save the Account data in xml format and read it back. +* can populate a default root Account data in xml format if missing +* can update existing Account password stored in the storage + +{empty} + + +[Discrete] +==== 4.2.2. *_Part B_* +image::partb.png[width="200"] + +[Discrete] +=== Account storage +All accounts are stored in a file call `/data/accountlist.xml`. This file is generated on the fly during first launch and populated with a root account. By default, a root account is hardcoded into the application with the username `rootUser` and password `rootPassword` with the role `SUPER_USER`. + +The diagram below shows what happen when a user launches the application: + +.Activity diagram when user launches the application +image::accountstoragediagram.png[width="700"] + +Only a `SUPER_USER` is allowed to create a new account, either for himself, or on behalf of another person. The diagram below shows what happen when a user attempts to register a new account: + +.Activity diagram when user registers an account +image::accountcreationdiagram.png[width="700"] + +==== Design Considerations +===== Aspect: What file type to store user account as? +* *Alternative 1 (current choice):* Store it as a `xml` file locally. +** Pros: The code to write and read xml file is already present for adding address book contact initially in the Address Book - level 4 app. Hence, adopting this code and modifying it for account storage is easier than coming up with code from scratch. +** Cons: Xml file is relatively wordy and verbose with all the opening and closing tags. For the same amount of account information, more data has to be stored using xml than format such as json. +* *Alternative 2:* Store it as a `json` file locally. +** Pros: Simpler syntax than `xml` and hence less data is required to store the same amount of account information. +** Pros: Can be parsed into a ready-to-use JavaScript object. +** Cons: Not familiar with json, hence more effort is needed to write code to store account in json format, compared to the already given code for xml storage. + +==== Security Considerations + +===== Database +Currently, the list of accounts is stored locally on data/accountlist.xml. For security purposes, we may consider the following implementations in the future for v2.0: + +* **Encrypt accountlist.xml:** This can prevent direct lookup of the file as the content is encrypted +* **Store the file on a server:** Due to project restriction, we are unable to implement this at v1.4. Storing file on a server has an added advantage of utilising web security practises or employing third party services to help protect our account list in private servers. + +===== Storing password +Username is stored in plaintext in accountlist.xml, as username is not private information. However, user password is hashed with `PBKDF2WithHmacSHA512` algorithm together with a `salt`, to prevent password from being visible in plaintext. `PBKDF2WithHmacSHA512` is deliberately chosen as it is a link:https://adambard.com/blog/3-wrong-ways-to-store-a-password/[slower] algorithm, thus slowing down brute-force attack for finding out user password. The hashing algorithm is present in `PasswordAuthentication` class and the implementation is based off this link:http://stackoverflow.com/a/2861125/3474[stackoverflow] answer. + +{empty} + + +==== 4.2.3. *_Part C_* +image::partc.png[width="200"] + +[discrete] +=== Use case: Login + +*MSS* + +1. User requests to log in with his username and password +2. System validates the information entered and allows the user access to the System +3. User is successfully logged in ++ +Use case ends. + +*Extensions* + +[none] +* 1a. User enters an incorrect username + ++ +[none] +** 1a1. The system display an error message and prompts the user to re-enter his username ++ +[none] +** Use case resumes from step 1. + +[none] +* 1b. User enters an incorrect password + ++ +[none] +** 1b1. The system will request the user to re-enter his password ++ +[none] +** 1b2. The user attempts to enter his password ++ +[none] +*** 1b2.1 The system determines that the password is incorrect and provides the option for user to retrieve his forgotten password (coming in v2.0) ++ +[none] +** Steps 1b1 and 1b2 are repeated until the user enters his correct password ++ +[none] +** Use case resumes from step 3. + +image:quoteend.png[width="180"] diff --git a/docs/team/wailunlim.adoc b/docs/team/wailunlim.adoc new file mode 100644 index 000000000000..a2230095eb63 --- /dev/null +++ b/docs/team/wailunlim.adoc @@ -0,0 +1,347 @@ += Lim Wai Lun - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets +:sectnums: + +[none] +== PROJECT: Heart² + +This portfolio aims to document my contributions to the project *_Heart²_*. + +GitHub: https://github.com/CS2103-AY1819S1-F10-3/main + +== Overview + +*_Heart²_* is a desktop wedding application for wedding planners to efficiently manage their clients and vendors. +*_Heart²_* allows wedding planners to quickly find suitable clients and vendors by automatically matching them together based on the vendors' services and the client's requirements. +The user primarily interacts with *_Heart²_* through a CLI, and it has a GUI created with JavaFX. +*_Heart²_* is written in Java, with about 10 kLoC. + +== Summary of contributions + +=== *Major enhancement 1*: + +Enabled support for two different types of contacts, clients and vendors, in *_Heart²_*. + +** *What it does*: + +Allows the user to be work with two different types of contacts using *_Heart²_*. + +** *Justification*: + +*_Heart²_* is made for wedding planners to efficiently manage their clients and vendors, two of their key stakeholders. +Therefore, we chose to model the contacts in *_Heart²_* after clients and vendors. + +** *Highlights*: + +. This enhancement serves a foundation of many features in *_Heart²_*, including: +`add`, `delete`, `update`, `view`, `list`, `addservice` and `automatch`. + +. In *_Heart²_*, the users are presented with two separated lists. +However, in *_Heart²_*, there is only one contact list for both clients and vendors. +Filters are constantly applied to this contact list as commands are entered so as to maintain the illusion of a distinct list to the user. + +. This enhancement required extensive refactoring of the codebase as the inherited, legacy code only supported a single contact. +*_Heart²_* currently supports two types of contacts, and can easily be extended to support even more types of contacts with this enhancement. + +=== *Major enhancement 2*: + +Implemented unique IDs for contacts. + +** *What it does*: + +Allows the user to refer to contacts by their unique IDs. + +** *Justification*: + +The inherited implementation from AddressBook (Level 4) had users use the contact's relative position in the currently shown list to refer to that contact. +For our users to make use of their time most efficiently, we chose to have our users be able to refer to contacts using IDs, as opposed to the previous implementation. + +In the previous, inherited implementation from AddressBook (Level 4), the user must ensure that the current list shown in the UI contains the contact *and* check the contact's relative position on the list. +With unique IDs for contacts, the context of the commands do not change, and this allows our users to know quickly the context of their commands and thus execute any command at any point of time in *_Heart²_*. + +With this unique ID system, our users are able to simplify and improve their workflow on *_Heart²_*, + +** *Highlights*: + +. This enhancement serves as another foundation of many features in *_Heart²_*, including: +`delete`, `update`, `view`, `addservice` and `automatch`. + +. Together with the contact type, it allows *_Heart²_* to be able to offer a very simple and intuitive command syntax. ++ +Commands pertaining to a contact type start with the contact type (`client` or `vendor`). +More specific commands pertaining to a specific contact have the unique ID appended to the contact type. + +=== *Minor enhancement 1*: + +Updated the main parser that parses all commands entered by the user. + +** *What it does*: + +Implements the command syntax enabled by the two previously mentioned enhancements. + +** *Highlights*: + +. This enhancement although minor proved to be quite a challenge, because commands differ in their components. ++ +For example, `help` consists of just a word, `client list` consists of two words, `vendor#3 delete` consists of 2 words and the unique ID, and lastly `client#1 update n/Wai Lun` consists of 2 words, the unique ID and the parameters for the command. ++ +It required some research on regular expressions in Java and tinkering in JShell. +In the end, only a single parser is used to parse and identify all of *_Heart²_* commands when they are entered by the user. + + +=== *Minor enhancement 2*: + +Extended the `add`, `delete`, `update` and `view` commands in *_Heart²_*. + +** *What it does*: + +Gives functionality to the mentioned commands. + +** *Highlights*: + +. This enhancement makes use of and relies on all three enhancements mentioned above. + +. The text feedback to the user is customised based on the command entered by the user. +Due to having two possibilities (`client` and `vendor`) for these commands, and in order avoid "hard-coding" every possible text feedback to the user, these text feedback use Java's string formatting feature. ++ +With this, the text feedback can be easily extended for even more contact types if we choose to add more contact types in the future. + + +=== *Code contributed*: + +As of 12 November 2018, I have contributed over 40 https://github.com/CS2103-AY1819S1-F10-3/main/pulls?utf8=✓&q=is%3Apr+author%3Awailunlim+is%3Aclosed[pull requests] on *_Heart²_*'s GitHub. + +The following pull requests are some examples of my code contribution, both functional and test code, to the *_Heart²_* project. + +. https://github.com/CS2103-AY1819S1-F10-3/main/pull/83[Support for different types of contacts.] +. https://github.com/CS2103-AY1819S1-F10-3/main/pull/161[Implementing the unique ID feature.] +. https://github.com/CS2103-AY1819S1-F10-3/main/pull/193[Displaying the correct text feedback to the user.] + + +. https://github.com/CS2103-AY1819S1-F10-3/main/pull/300[Adding tests for uncovered lines.] +. https://github.com/CS2103-AY1819S1-F10-3/main/pull/303[Finding and fixing bugs, and adding test cases to cover them.] + +My full code contribution to *_Heart²_* can be viewed https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=wailunlim&sort=displayName&since=2018-09-12&until=2018-11-12&timeframe=day&reverse=false&repoSort=true[here]. + +=== *Other contributions*: + +. Helped solve bug fixes in *_Heart²_*. + +. Helped with checking all documents for spelling and grammar mistakes. + +. As of 12 November 2018: +.. Reviewed 21 link:https://github.com/CS2103-AY1819S1-F10-3/main/pulls?utf8=✓&q=is%3Apr+reviewed-by%3Awailunlim+[pull requests] on *_Heart²_*'s Github. +.. Raised 21 link:https://github.com/CS2103-AY1819S1-F10-3/main/issues?utf8=✓&q=is%3Aclosed+is%3Aissue+author%3Awailunlim+[issues] on *_Heart²_*'s Github. + +. Restructured and generalised the commands regarding `client` and `vendor` in the user guide for a better reading experience. + +. Participated actively in discussions regarding *_Heart²_* and its features. + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide._ +|=== + +=== Working with a specific contact + +Contacts in *_Heart²_* are assigned a unique ID each for you to refer to the contact at any point of time when using *_Heart²_*. + + +[TIP] +Commands pertaining to one specific contact have an additional unique ID appended to the back of `client` or `vendor`. + +[TIP] +These IDs are persistent for one session. Each restart of Heart reassigns IDs to contacts, effectively accounting for deleted contacts and compacting the IDs of your contacts. + +[NOTE] +IDs can be similar for `client` and `vendor`. However, since the contact type and ID come hand in hand, the contacts are still effectively unique! + +==== Viewing a contact: `view` + +You can `view` detailed information about a specific contact using its unique ID. +This information will be displayed on the right panel in *_Heart²_*. + +[TIP] +`view` shows you all the information regarding the specific client, which includes the name, phone number, email address, tags, residential (client) or office (vendor) address and services requested (client) or offered (vendor). + +[NOTE] +*Any* command following `view` will hide the information shown before on the panel on the right. + +Format: `# view` + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client#3 view` +|===== +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor#3 view` +|===== + +[TIP] +Clicking on the contact panels on the left of *_Heart²_*'s GUI corresponds to a `view` command for that contact! + +==== Deleting a contact: `delete` + +You can also `delete` a contact from *_Heart²_*, by specifying its unique ID. + +Format: `# delete` + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client#123 delete` +|===== +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor#123 delete` +|===== + +[TIP] +You can `undo` and `redo` deleting a contact! + +==== Updating a contact: `update` + +You can also `update` a contact's particulars, again by specifying its unique ID, followed by the updated fields. + +Format: `# update [n/FULL_NAME] [p/PHONE_NUMBER] [e/EMAIL_ADDRESS] [a/HOME_ADDRESS] [t/TAG]...` + +[NOTE] +You cannot update a contact to another contact that already exists in the application. Two contacts are considered duplicates if they are the same type (either both clients or both vendors), possess the same name, and either the same phone number or the same email address. + +[NOTE] +When editing tags, adding of tags is not cumulative and existing tags of the contact will be removed. + +[TIP] +You can remove all tags for a contact by updating the contact with `t/` (without specifying any tags after it)! + +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`client#123 update n/Jane Doe e/janedoe@gmail.com` +|===== +[cols="^,<5a", frame=none] +|===== +|image:exampleimage.png[width="64", role="center"] +|`vendor#123 update n/Bob Vans e/bobvans@gmail.com` +|===== + +[TIP] +You can `undo` and `redo` updating a contact! + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide._ +|=== + +=== Unique ID feature +*_Heart²_* assigns a unique ID to every `client` and `vendor` when they are added into *_Heart²_*. +This ID is unique within their contact type, meaning that a `client` and a `vendor` may have the same ID, but since this ID comes hand in hand with the contact type, they are effectively unique. +These IDs are last for a single session, and *_Heart²_* reassigns the IDs at the start of the next session. + +==== Current Implementation +Both the `Client` and `Vendor` class have a `public static` running counter starting from 1. +When a `client` or `vendor` is created, it is assigned that number, before incrementing it by 1. +The `contact` then has this ID for this session, and the user can use this ID, coupled with the contact type to always refer to this particular contact. + +This unique ID is used by many other commands, namely: `add`, `delete`, `update`, `view`, `addservice`, `automatch`. +It allows for these commands to be executed at any point in *_Heart²_*, with always the same context. + +==== Design Considerations +===== Aspect: How should we refer to contacts in *_Heart²_*? + +* *Alternative 1*: +Use the legacy implementation, which is to use the relative position of the contact in the list. + +** Pros: No change is required, as it is the legacy implementation. + +** Cons: Users have to navigate to a list that shows that contact, and the relative position of that contact may keep changing throughout a session. + +* *Alternative 2* (current choice): + +** Pros: Users are able to refer back to a particular contact at any time, without requiring the current list shown to contain that contact. +Also, this ID will never change during a session, so the user can confidently use the ID knowing that it will always refer to that contact. + +** Cons: Users still have to remember this unique ID to refer back to the contact. It might be hard to remember the ID. + +After much consideration, we decided to go with option 2. +*_Heart²_* is built for speed, and we would like to give our users flexibility to execute any command within *_Heart²_* at any time. +We believe that this can give users more control and power over their work using *_Heart²_*, and therefore we chose to implement this unique ID system. + +However, we also do realise that users might find it hard to remember the unique ID assigned to the contact. +While users can quickly look at a recent contact using the command `history`, a possibly quality-of-life improvement would be to implement a mnemonic unique identifier. + +=== Add contact feature +*_Heart²_* requires users to explicitly specify whether the contact to be added is a `client` or a `vendor` in the command. + +* `client add n/Wai Lun p/90463327 e/wailun@u.nus.edu a/PGP House` +* `vendor add n/Lun Wai p/72336409 e/lunwai@u.nus.edu a/RVRC` + +The above commands add a `client` and a `vendor`, together with the details provided, respectively. + +This differentiation between `client` and `vendor` facilitates many other features of *_Heart²_*. +It complements the unique ID feature earlier to ensure that a `client` and a `vendor` with the same ID are still differentiable due to the contact type. + +Adding of duplicate contacts are not allowed in *_Heart²_*. +[NOTE] +A contact is considered a duplicate if they are of the same contact type *and* have the same name *and* have *either* the same phone number *or* email address. + +==== Current Implementation +Both `Client` and `Vendor` classes inherit from an abstract `Contact` class. +When adding a contact, either a new `Client` or a `Vendor` object is instantiated. +Both `Client` and `Vendor` objects are added to a list of generic type `Contact`. + +In order to differentiate them, there is an abstract method `Contact#getType()` that `Client` and `Vendor` implement differently. +`Client` objects return a `ContactType.CLIENT` enum while `Vendor` objects return a `ContactType.VENDOR` enum. + +When adding a contact, the parser first distinguishes whether it is an addition of a `client` or `vendor`. +The correct `ContactType` enum is then passed to `AddCommandParser`. +The `AddCommandParser` parses the argument to create the appropriate contact, and creates the appropriate `AddCommand`. + +This `AddCommand` is passed back to the `LogicManager`, and the method `execute()` is called. +The contact is then added to the model. + +Below is the sequence diagram of a `client add` command. + +image::AddClientSequenceDiagram.png[width="800"] + + + +==== Design Considerations +===== Aspect: How should we store `Client` and `Vendor` objects in *_Heart²_*? + +* *Alternative 1* (current choice): +`Client` and `Vendor` objects are stored in a more general `Contact` list. +** Pros: Easy to implement by tweak the inherited legacy list slightly. + +** Cons: Cannot tell immediately if an element in the `Contact` list is a `Client` or `Vendor`. +This might take a longer time to display lists, due to having to filter them every time. + +* *Alternative 2*: +Hold `Client` and `Vendor` objects differently in two different lists. +** Pros: Able to get `Client` or `Vendor` immediately without having to go through the entire `Contact` list as in alternative 1. +** Cons: Difficult and extremely tedious to implement. + +===== Aspect: How restrictive should the definition of a duplicate contact be? + +* *Alternative 1*: +It should be regardless of contact type, meaning a `client` and a `vendor` cannot have the same name *and* either the same phone number *or* email. + +** Pros: No additional implementation required. The legacy implementation already supports this. + +** Cons: Less flexibility for our users. A `client` cannot be a `vendor` possibly. + +* *Alternative 2* (current choice): +A `client` and a `vendor` can have similar fields, meaining a `client` and a `vendor` can possibly have the same name, phone number *and/or* email. + +** Pros: More flexibility for our users. A `client` can be a `vendor` too, which is possible in the real world. + +** Cons: Additional implementation to have. diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index ecdd043a4f81..364779cdd626 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -25,12 +25,15 @@ import seedu.address.model.ModelManager; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; +import seedu.address.model.account.Account; import seedu.address.model.util.SampleDataUtil; +import seedu.address.storage.AccountStorage; import seedu.address.storage.AddressBookStorage; import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; import seedu.address.storage.UserPrefsStorage; +import seedu.address.storage.XmlAccountStorage; import seedu.address.storage.XmlAddressBookStorage; import seedu.address.ui.Ui; import seedu.address.ui.UiManager; @@ -40,9 +43,9 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 6, 0, true); + public static final Version VERSION = new Version(1, 4, 0, true); - private static final Logger logger = LogsCenter.getLogger(MainApp.class); + protected static final Logger LOGGER = LogsCenter.getLogger(MainApp.class); protected Ui ui; protected Logic logic; @@ -54,7 +57,7 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + LOGGER.info("=============================[ Initializing Heart² ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -74,6 +77,8 @@ public void init() throws Exception { ui = new UiManager(logic, config, userPrefs); initEventsCenter(); + + initAccountStorage(); } /** @@ -87,14 +92,14 @@ private Model initModelManager(Storage storage, UserPrefs 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 a sample Heart²"); } 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 an empty Heart²"); 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 an empty Heart²"); initialData = new AddressBook(); } @@ -117,17 +122,17 @@ protected Config initConfig(Path configFilePath) { configFilePathUsed = Config.DEFAULT_CONFIG_FILE; if (configFilePath != null) { - logger.info("Custom Config file specified " + configFilePath); + LOGGER.info("Custom Config file specified " + configFilePath); configFilePathUsed = configFilePath; } - logger.info("Using config file : " + configFilePathUsed); + LOGGER.info("Using config file : " + configFilePathUsed); try { Optional configOptional = ConfigUtil.readConfig(configFilePathUsed); initializedConfig = configOptional.orElse(new Config()); } catch (DataConversionException e) { - logger.warning("Config file at " + configFilePathUsed + " is not in the correct format. " + LOGGER.warning("Config file at " + configFilePathUsed + " is not in the correct format. " + "Using default config properties"); initializedConfig = new Config(); } @@ -136,7 +141,7 @@ protected Config initConfig(Path configFilePath) { try { ConfigUtil.saveConfig(initializedConfig, configFilePathUsed); } catch (IOException e) { - logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + LOGGER.warning("Failed to save config file : " + StringUtil.getDetails(e)); } return initializedConfig; } @@ -148,18 +153,18 @@ protected Config initConfig(Path configFilePath) { */ protected UserPrefs initPrefs(UserPrefsStorage storage) { Path prefsFilePath = storage.getUserPrefsFilePath(); - logger.info("Using prefs file : " + prefsFilePath); + LOGGER.info("Using prefs file : " + prefsFilePath); UserPrefs initializedPrefs; try { Optional prefsOptional = storage.readUserPrefs(); initializedPrefs = prefsOptional.orElse(new UserPrefs()); } catch (DataConversionException e) { - logger.warning("UserPrefs file at " + prefsFilePath + " is not in the correct format. " + LOGGER.warning("UserPrefs file at " + prefsFilePath + " is not in the correct format. " + "Using default user prefs"); initializedPrefs = new UserPrefs(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); + LOGGER.warning("Problem while reading from the file. Will be starting with an empty Heart²"); initializedPrefs = new UserPrefs(); } @@ -167,7 +172,7 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { try { storage.saveUserPrefs(initializedPrefs); } catch (IOException e) { - logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + LOGGER.warning("Failed to save config file : " + StringUtil.getDetails(e)); } return initializedPrefs; @@ -179,18 +184,18 @@ private void initEventsCenter() { @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + LOGGER.info("Starting Heart² " + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + LOGGER.info("============================ [ Stopping Heart² ] ============================="); ui.stop(); try { storage.saveUserPrefs(userPrefs); } catch (IOException e) { - logger.severe("Failed to save preferences " + StringUtil.getDetails(e)); + LOGGER.severe("Failed to save preferences " + StringUtil.getDetails(e)); } Platform.exit(); System.exit(0); @@ -198,11 +203,23 @@ public void stop() { @Subscribe public void handleExitAppRequestEvent(ExitAppRequestEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); + LOGGER.info(LogsCenter.getEventHandlingLogMessage(event)); stop(); } + /** + * The main entry of the application. + * @param args not accepting arguments + */ public static void main(String[] args) { launch(args); } + + /** + * Initialize the storage used and populate default root password if file does not exist. + */ + private static void initAccountStorage() { + AccountStorage accountStorage = new XmlAccountStorage(); + accountStorage.populateRootAccountIfMissing(Account.getRootAccount()); + } } diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/seedu/address/commons/core/Config.java index e978d621e086..9d4d8010ab48 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/seedu/address/commons/core/Config.java @@ -13,7 +13,7 @@ public class Config { public static final Path DEFAULT_CONFIG_FILE = Paths.get("config.json"); // Config values customizable through config file - private String appTitle = "Address App"; + private String appTitle = "Heart²"; private Level logLevel = Level.INFO; private Path userPrefsFilePath = Paths.get("preferences.json"); diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/address/commons/core/LogsCenter.java index 5316a1d87d3e..3de9032f733b 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/address/commons/core/LogsCenter.java @@ -29,7 +29,7 @@ public class LogsCenter { /** * Initializes with a custom log level (specified in the {@code config} object) * Loggers obtained *AFTER* this initialization will have their logging level changed
- * Logging levels for existing loggers will only be updated if the logger with the same name + * Logging levels for existing loggers will only be updated if the LOGGER with the same name * is requested again from the LogsCenter. */ public static void init(Config config) { @@ -38,7 +38,7 @@ public static void init(Config config) { } /** - * Creates a logger with the given name. + * Creates a LOGGER with the given name. */ public static Logger getLogger(String name) { Logger logger = Logger.getLogger(name); @@ -62,7 +62,7 @@ public static Logger getLogger(Class clazz) { } /** - * Adds the {@code consoleHandler} to the {@code logger}.
+ * Adds the {@code consoleHandler} to the {@code LOGGER}.
* Creates the {@code consoleHandler} if it is null. */ private static void addConsoleHandler(Logger logger) { @@ -73,7 +73,7 @@ private static void addConsoleHandler(Logger logger) { } /** - * Remove all the handlers from {@code logger}. + * Remove all the handlers from {@code LOGGER}. */ private static void removeHandlers(Logger logger) { Arrays.stream(logger.getHandlers()) @@ -81,7 +81,7 @@ private static void removeHandlers(Logger logger) { } /** - * Adds the {@code fileHandler} to the {@code logger}.
+ * Adds the {@code fileHandler} to the {@code LOGGER}.
* Creates {@code fileHandler} if it is null. */ private static void addFileHandler(Logger logger) { @@ -91,7 +91,7 @@ private static void addFileHandler(Logger logger) { } logger.addHandler(fileHandler); } catch (IOException e) { - logger.warning("Error adding file handler for logger."); + logger.warning("Error adding file handler for LOGGER."); } } diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e4695..353a7cde9487 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -7,7 +7,10 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_INVALID_CONTACT_FORMAT = "Invalid contact format: %s"; + public static final String MESSAGE_INVALID_CONTACT_TYPE = "Invalid contact type: %s"; + public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The unique ID #%1$s is invalid"; + public static final String MESSAGE_LIST_ALL_X = "Listed all %1$s(s)"; + public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d %2$s(s) listed!"; } diff --git a/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java b/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java index b72ad4740e5a..b0c4a7577785 100644 --- a/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java +++ b/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java @@ -14,6 +14,6 @@ public AddressBookChangedEvent(ReadOnlyAddressBook data) { @Override public String toString() { - return "number of persons " + data.getPersonList().size(); + return "number of persons " + data.getContactList().size(); } } diff --git a/src/main/java/seedu/address/commons/events/ui/ClearBrowserPanelRequestEvent.java b/src/main/java/seedu/address/commons/events/ui/ClearBrowserPanelRequestEvent.java new file mode 100644 index 000000000000..5180e853e5a8 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/ClearBrowserPanelRequestEvent.java @@ -0,0 +1,15 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; + +/** + * Represents a request to clear the browser panel. + */ +public class ClearBrowserPanelRequestEvent extends BaseEvent { + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/DeselectRequestEvent.java b/src/main/java/seedu/address/commons/events/ui/DeselectRequestEvent.java new file mode 100644 index 000000000000..042fa795a4ba --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/DeselectRequestEvent.java @@ -0,0 +1,13 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; + +/** + * Represents a request to deselect selection, if any. + */ +public class DeselectRequestEvent extends BaseEvent { + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/address/commons/events/ui/DisplayAutoMatchResultRequestEvent.java b/src/main/java/seedu/address/commons/events/ui/DisplayAutoMatchResultRequestEvent.java new file mode 100644 index 000000000000..426b6912b771 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/DisplayAutoMatchResultRequestEvent.java @@ -0,0 +1,15 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; + +/** + * An event requesting to display automatch results in the table. + */ +public class DisplayAutoMatchResultRequestEvent extends BaseEvent { + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/LoginSuccessEvent.java b/src/main/java/seedu/address/commons/events/ui/LoginSuccessEvent.java new file mode 100644 index 000000000000..68622a9dc115 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/LoginSuccessEvent.java @@ -0,0 +1,15 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; + +/** + * An event indicating a successful login. + */ +public class LoginSuccessEvent extends BaseEvent { + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/LogoutRequestEvent.java b/src/main/java/seedu/address/commons/events/ui/LogoutRequestEvent.java new file mode 100644 index 000000000000..c6ceb34ecb68 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/LogoutRequestEvent.java @@ -0,0 +1,15 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; + +/** + * An event requesting to logout. + */ +public class LogoutRequestEvent extends BaseEvent { + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java b/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java index c5c8b9ce90ed..d78cda87e5ee 100644 --- a/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java +++ b/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java @@ -1,17 +1,17 @@ package seedu.address.commons.events.ui; import seedu.address.commons.events.BaseEvent; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** - * Represents a selection change in the Person List Panel + * Represents a selection change in the Client List Panel */ public class PersonPanelSelectionChangedEvent extends BaseEvent { - private final Person newSelection; + private final Contact newSelection; - public PersonPanelSelectionChangedEvent(Person newSelection) { + public PersonPanelSelectionChangedEvent(Contact newSelection) { this.newSelection = newSelection; } @@ -20,7 +20,7 @@ public String toString() { return getClass().getSimpleName(); } - public Person getNewSelection() { + public Contact getNewSelection() { return newSelection; } } diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/seedu/address/commons/util/CollectionUtil.java index eafe4dfd6818..2223d817924b 100644 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ b/src/main/java/seedu/address/commons/util/CollectionUtil.java @@ -4,9 +4,14 @@ import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import java.util.stream.Stream; +import seedu.address.model.ContactType; +import seedu.address.model.contact.Contact; + /** * Utility methods related to Collections */ @@ -32,4 +37,23 @@ public static void requireAllNonNull(Collection items) { public static boolean isAnyNonNull(Object... items) { return items != null && Arrays.stream(items).anyMatch(Objects::nonNull); } + + /** + * Returns the contactType of the contact that is different between the 2 lists. + * @param list1 First list to compare. + * @param list2 Second list to compare. + * @return ContactType of the different contact between the 2 lists. + */ + public static ContactType compareListOfContacts(List list1, List list2) { + + List diffList = list1.stream().filter(contact -> !list2.stream() + .anyMatch(contactToCheck -> contactToCheck.equals(contact))).collect(Collectors.toList()); + + if (diffList.isEmpty()) { + diffList = list2.stream().filter(contact -> !list1.stream() + .anyMatch(contactToCheck -> contactToCheck.equals(contact))).collect(Collectors.toList()); + } + + return diffList.get(0).getType(); + } } diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb8..5888efb23c6f 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -38,6 +38,30 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { .anyMatch(preppedWord::equalsIgnoreCase); } + /** + * Returns true if the {@code sentence} contains the {@code substring}. + * Ignores case, but a full substring match is required. + *
examples:
+     *       containsWordIgnoreCase("ABc def", "abc") == true
+     *       containsWordIgnoreCase("ABc def", "DEF") == true
+     *       containsWordIgnoreCase("ABc def", "AB") == true
+     *       containsWordIgnoreCase("ABc def", "ABcd") == false
+     *       
+ * @param sentence cannot be null + * @param substring cannot be null, cannot be empty + */ + public static boolean containsIgnoreCase(String sentence, String substring) { + requireNonNull(sentence); + requireNonNull(substring); + + String preppedSubstring = substring.trim().replaceAll(" +", " ").toLowerCase(); + checkArgument(!preppedSubstring.isEmpty(), "Substring parameter cannot be empty"); + + String preppedSentence = sentence.toLowerCase(); + + return preppedSentence.contains(preppedSubstring); + } + /** * Returns a detailed message of the t, including the stack trace. */ diff --git a/src/main/java/seedu/address/commons/util/XmlUtil.java b/src/main/java/seedu/address/commons/util/XmlUtil.java index a78cd15b7f0c..b1e24b2f1d32 100644 --- a/src/main/java/seedu/address/commons/util/XmlUtil.java +++ b/src/main/java/seedu/address/commons/util/XmlUtil.java @@ -11,6 +11,10 @@ import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.account.AccountList; +import seedu.address.storage.XmlSerializableAccountList; + /** * Helps with reading from and writing to XML files. */ @@ -68,4 +72,41 @@ public static void saveDataToFile(Path file, T data) throws FileNotFoundExce m.marshal(data, file.toFile()); } + /** + * Update the user with the {@code currentAccount} to the new password. + * @param file The valid xml file containing the account list stored. + * @param username The associated username of which the password is to be changed. + * @param newPassword The new password to change to + * @throws IllegalValueException Thrown if duplicated account was found in the account list. + * @throws JAXBException Thrown if there is an error during converting the data + * into xml and writing to the file. + * @throws FileNotFoundException Thrown if the account list file cannot be found. + */ + public static void updatePasswordInFile(Path file, String username, String newPassword) + throws IllegalValueException, JAXBException, FileNotFoundException { + requireNonNull(file); + requireNonNull(username); + requireNonNull(newPassword); + + if (!Files.exists(file)) { + throw new FileNotFoundException("File not found : " + file.toAbsolutePath()); + } + + JAXBContext context = JAXBContext.newInstance(XmlSerializableAccountList.class); + + // read old file + Unmarshaller um = context.createUnmarshaller(); + XmlSerializableAccountList currentList = (XmlSerializableAccountList) um.unmarshal(file.toFile()); + + // update password + AccountList accountList = currentList.toModelType(); + + accountList.updatePassword(username, newPassword); + XmlSerializableAccountList newUpdatedList = new XmlSerializableAccountList(accountList); + + // overwrite old file + Marshaller m = context.createMarshaller(); + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + m.marshal(newUpdatedList, file.toFile()); + } } diff --git a/src/main/java/seedu/address/logic/CommandHistory.java b/src/main/java/seedu/address/logic/CommandHistory.java index 39bca9b8df57..b6c216f9aa17 100644 --- a/src/main/java/seedu/address/logic/CommandHistory.java +++ b/src/main/java/seedu/address/logic/CommandHistory.java @@ -34,6 +34,13 @@ public List getHistory() { return new LinkedList<>(userInputHistory); } + /** + * Delete the entire command history. + */ + public void clear() { + userInputHistory.clear(); + } + @Override public boolean equals(Object obj) { // short circuit if same object diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 8b34b862039a..e6edbb0817d8 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -3,8 +3,10 @@ import javafx.collections.ObservableList; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.commands.exceptions.LackOfPrivilegeException; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Person; +import seedu.address.model.AutoMatchResult; +import seedu.address.model.contact.Contact; /** * API of the Logic component @@ -16,12 +18,19 @@ public interface Logic { * @return the result of the command execution. * @throws CommandException If an error occurs during command execution. * @throws ParseException If an error occurs during parsing. + * @throws LackOfPrivilegeException If the user account does not have the privilege to + * execute that command. */ - CommandResult execute(String commandText) throws CommandException, ParseException; + CommandResult execute(String commandText) throws CommandException, ParseException, LackOfPrivilegeException; /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); + ObservableList getFilteredPersonList(); /** Returns the list of input entered by the user, encapsulated in a {@code ListElementPointer} object */ ListElementPointer getHistorySnapshot(); + + /** + * Retrieves the last updated results for the auto-matching. + */ + AutoMatchResult getAutoMatchResult(); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9aff86fc33dc..3596bc83b5aa 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -7,16 +7,24 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.EditPasswordCommand; +import seedu.address.logic.commands.LoginCommand; +import seedu.address.logic.commands.LogoutCommand; +import seedu.address.logic.commands.RegisterAccountCommand; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.commands.exceptions.LackOfPrivilegeException; import seedu.address.logic.parser.AddressBookParser; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.AutoMatchResult; import seedu.address.model.Model; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** * The main LogicManager of the app. */ public class LogicManager extends ComponentManager implements Logic { + private static final String PARAMETER_CENSORED = " "; + private final Logger logger = LogsCenter.getLogger(LogicManager.class); private final Model model; @@ -30,23 +38,47 @@ public LogicManager(Model model) { } @Override - public CommandResult execute(String commandText) throws CommandException, ParseException { + public CommandResult execute(String commandText) throws CommandException, ParseException, LackOfPrivilegeException { logger.info("----------------[USER COMMAND][" + commandText + "]"); + Command command = null; + try { - Command command = addressBookParser.parseCommand(commandText); + if (model.isUserLoggedIn()) { + command = addressBookParser.parseCommand(commandText); + } else { + command = addressBookParser.parseCommandBeforeLoggedIn(commandText); + } return command.execute(model, history); } finally { - history.add(commandText); + // do not add to history if logging out, as it is terminating a session. + if (!commandText.equals(LogoutCommand.COMMAND_WORD)) { + // if history is any of these instances, censored the parameter as it contains + // sensitive information + if (command instanceof LoginCommand) { + history.add(LoginCommand.COMMAND_WORD + PARAMETER_CENSORED); + } else if (command instanceof RegisterAccountCommand) { + history.add(RegisterAccountCommand.COMMAND_WORD + PARAMETER_CENSORED); + } else if (command instanceof EditPasswordCommand) { + history.add(EditPasswordCommand.COMMAND_WORD + PARAMETER_CENSORED); + } else { + history.add(commandText); + } + } } } @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); + public ObservableList getFilteredPersonList() { + return model.getFilteredContactList(); } @Override public ListElementPointer getHistorySnapshot() { return new ListElementPointer(history.getHistory()); } + + @Override + public AutoMatchResult getAutoMatchResult() { + return model.getAutoMatchResult(); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index d88e831ff1ce..9fbbe31f0d3d 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -7,26 +7,35 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.DeselectRequestEvent; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.commands.exceptions.LackOfPrivilegeException; import seedu.address.model.Model; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** - * Adds a person to the address book. + * Adds a contact to the address book. */ public class AddCommand extends Command { - public static final String COMMAND_WORD = "add"; + public static final String COMMAND_WORD_GENERAL = "%1$s add"; + /* + the below are necessary for switch statements as a constant expression is required for to compile switch + statements + */ + public static final String COMMAND_WORD_CLIENT = "client add"; + public static final String COMMAND_WORD_VENDOR = "vendor add"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + public static final String MESSAGE_USAGE = COMMAND_WORD_GENERAL + ": Adds a %1$s to the application. " + "Parameters: " + PREFIX_NAME + "NAME " + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " + + "Example: " + COMMAND_WORD_GENERAL + " " + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "98765432 " + PREFIX_EMAIL + "johnd@example.com " @@ -34,30 +43,36 @@ public class AddCommand extends Command { + 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"; + public static final String MESSAGE_SUCCESS = "New %1$s added: %2$s"; + public static final String MESSAGE_DUPLICATE_CONTACT = "This contact already exists in the application."; - private final Person toAdd; + private final Contact toAdd; /** - * Creates an AddCommand to add the specified {@code Person} + * Creates an AddCommand to add the specified {@code Contact} */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; + public AddCommand(Contact contact) { + requireNonNull(contact); + toAdd = contact; } @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { + public CommandResult execute(Model model, CommandHistory history) throws CommandException, + LackOfPrivilegeException { requireNonNull(model); - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + if (!model.getUserAccount().hasWritePrivilege()) { + throw new LackOfPrivilegeException(String.format(COMMAND_WORD_GENERAL, toAdd.getType())); + } + + if (model.hasContact(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_CONTACT); } - model.addPerson(toAdd); + model.addContact(toAdd); model.commitAddressBook(); - return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + EventsCenter.getInstance().post(new DeselectRequestEvent()); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd.getType(), toAdd)); } @Override diff --git a/src/main/java/seedu/address/logic/commands/AddServiceCommand.java b/src/main/java/seedu/address/logic/commands/AddServiceCommand.java new file mode 100644 index 000000000000..8221ba92ed92 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddServiceCommand.java @@ -0,0 +1,140 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SERVICE; + +import java.util.List; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.DeselectRequestEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.commands.exceptions.LackOfPrivilegeException; +import seedu.address.model.ContactType; +import seedu.address.model.Model; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Service; + +/** + * Adds a service and budget under a client. + */ +public class AddServiceCommand extends Command { + + public static final String COMMAND_WORD = "addservice"; + public static final String COMMAND_WORD_GENERAL = "%1$s%2$s addservice"; + public static final String COMMAND_WORD_CLIENT = "client addservice"; + public static final String COMMAND_WORD_VENDOR = "vendor addservice"; + + public static final String MESSAGE_USAGE = COMMAND_WORD_GENERAL + ": Adds a service to the %1$s identified " + + "by the assigned unique %1$s ID.\n" + + "Parameters: # (must be a positive integer) " + + PREFIX_SERVICE + "SERVICE " + + PREFIX_COST + "COST (excluding symbols to 2 decimal places) \n" + + "Example: " + String.format(COMMAND_WORD_GENERAL, "%1$s", "#3") + " " + + PREFIX_SERVICE + "photographer " + + PREFIX_COST + "1000.00 \n" + + Service.MESSAGE_SERVICE_NAME_CONSTRAINTS + "\n" + + Service.MESSAGE_SERVICE_COST_CONSTRAINTS; + + public static final String MESSAGE_ADD_SERVICE_SUCCESS = "New service added for : %1$s \n" + "Service: %2$s"; + public static final String MESSAGE_DUPLICATE_SERVICE = "This service has already been added"; + + private final Index id; + private final Service service; + private final ContactType contactType; + + /** + * Creates an AddServiceCommand to add the specified service and budget. + * @param id of the contact in the filtered contact list to add service to + * @param service service to add + * @param contactType specifies if contact is a client or service provider + */ + public AddServiceCommand(Index id, Service service, ContactType contactType) { + requireNonNull(id); + requireNonNull(service); + + this.id = id; + this.service = service; + this.contactType = contactType; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException, + LackOfPrivilegeException { + requireNonNull(model); + + if (!model.getUserAccount().hasWritePrivilege()) { + throw new LackOfPrivilegeException(String.format(COMMAND_WORD_GENERAL, contactType, "#")); + } + + // id is unique + model.updateFilteredContactList(contactType.getFilter().and(contact -> contact.getId() == id.getOneBased())); + + List filteredList = model.getFilteredContactList(); + + if (filteredList.size() == 0) { + // filtered list size is 0, meaning there is no such contact + model.updateFilteredContactList(contactType.getFilter()); + throw new CommandException(String.format(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, + id.getOneBased())); + } + + Contact contactToEdit = filteredList.get(0); + Contact editedContact; + + if (contactToEdit.hasService(service)) { + model.updateFilteredContactList(contactType.getFilter()); + throw new CommandException(MESSAGE_DUPLICATE_SERVICE); + } else { + editedContact = createContactWithService(contactToEdit, service); + } + + model.updateContact(contactToEdit, editedContact); + model.updateFilteredContactList(contactType.getFilter()); + model.commitAddressBook(); + EventsCenter.getInstance().post(new DeselectRequestEvent()); + return new CommandResult(String.format(MESSAGE_ADD_SERVICE_SUCCESS, contactToEdit.getName(), service)); + } + + /** + * Creates a new contact with the newly added service + * @param contact Current contact to add service to + * @param service Service to be added + * @return New contact with newly added service + */ + private Contact createContactWithService(Contact contact, Service service) { + UpdateCommand.EditContactDescriptor editContactDescriptor = new UpdateCommand.EditContactDescriptor(); + editContactDescriptor.setName(contact.getName()); + editContactDescriptor.setPhone(contact.getPhone()); + editContactDescriptor.setAddress(contact.getAddress()); + editContactDescriptor.setEmail(contact.getEmail()); + editContactDescriptor.setTags(contact.getTags()); + editContactDescriptor.setServices(contact.getServices()); + editContactDescriptor.addService(service); + + return UpdateCommand.createEditedContact(contact, editContactDescriptor, contactType); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddServiceCommand)) { + return false; + } + + // state check + AddServiceCommand e = (AddServiceCommand) other; + return id.equals(e.id) + && service.equals(e.service) + && contactType.equals(contactType); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/AutoMatchCommand.java b/src/main/java/seedu/address/logic/commands/AutoMatchCommand.java new file mode 100644 index 000000000000..2708b493208d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AutoMatchCommand.java @@ -0,0 +1,202 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; + +import java.util.Collection; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.events.ui.DeselectRequestEvent; +import seedu.address.commons.events.ui.DisplayAutoMatchResultRequestEvent; +import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.AutoMatchResult; +import seedu.address.model.ContactType; +import seedu.address.model.Model; +import seedu.address.model.client.Client; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Service; +import seedu.address.model.vendor.Vendor; + +/** + * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class AutoMatchCommand extends Command { + + public static final String COMMAND_WORD = "automatch"; + public static final String COMMAND_WORD_CLIENT = "client" + " " + COMMAND_WORD; + public static final String COMMAND_WORD_VENDOR = "vendor" + " " + COMMAND_WORD; + + public static final String MESSAGE_USAGE_CLIENT = COMMAND_WORD + ": Finds all vendors that can fulfil any of the" + + "service requirements of the client at his/her listed cost price." + + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: # (must be a positive integer)\n" + + "Example: client#123 " + COMMAND_WORD; + public static final String MESSAGE_USAGE_VENDOR = COMMAND_WORD + ": Finds all clients that has service " + + "requirements that can be served by the service provider at their listed cost price." + + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: # (must be a positive integer)\n" + + "Example: vendor#123 " + COMMAND_WORD; + + private final ContactType contactType; + private final int contactId; + + public AutoMatchCommand(String contactType, int contactId) throws ParseException, NullPointerException { + requireNonNull(contactType); + this.contactType = ContactType.fromString(contactType); + this.contactId = contactId; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + // Update filtered contact list to contain all contacts and get the list + EventsCenter.getInstance().post(new DeselectRequestEvent()); + model.updateFilteredContactList(x -> false); + model.getFilteredContactList(); + + Contact contact; + + try { + // Find the contact for which we are going to find matches for + contact = model + .getAddressBook() + .getContactList() + .stream() + .filter(c -> (contactType == ContactType.CLIENT && c instanceof Client) + || (contactType == ContactType.VENDOR && c instanceof Vendor)) + .filter(c -> c.getId() == contactId) + .findFirst() + .get(); + } catch (NoSuchElementException | NumberFormatException exception) { + throw new CommandException(String.format(MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, this.contactId)); + } + + AutoMatchResult autoMatchResult; + if (contact instanceof Client) { + Collection servicesRequired = contact.getServices().values(); + model.updateFilteredContactList(c -> c instanceof Vendor); + + // Perform auto-matching + autoMatchResult = model + .getFilteredContactList() + .stream() + .reduce(new AutoMatchResult(contact), (accumulatedResult, c) -> { + accumulatedResult.put(c, servicesCanBeFulfilledByVendor((Vendor) c, + servicesRequired)); + return accumulatedResult; + }, AutoMatchResult::mergeResults); + } else if (contact instanceof Vendor) { + Collection servicesProvided = contact.getServices().values(); + model.updateFilteredContactList(c -> c instanceof Client); + + // Perform auto-matching + autoMatchResult = model + .getFilteredContactList() + .stream() + .reduce(new AutoMatchResult(contact), (accumulatedResult, c) -> { + accumulatedResult.put(c, servicesNeededToBeFulfilledByClient((Client) c, + servicesProvided)); + return accumulatedResult; + }, AutoMatchResult::mergeResults); + } else { + // We should never arrive here. If we do, it means there's a Contact subclass that is not handled here. + throw new CommandException("Unknown entity, neither client nor service provider found in database."); + } + + // Use auto-matching results to filter contact list. + model.updateAutoMatchResult(autoMatchResult); + model.updateFilteredContactList(c -> autoMatchResult.getContacts().contains(c)); + + // Post event to display results in table. + EventsCenter.getInstance().post(new DisplayAutoMatchResultRequestEvent()); + + // Post event to display summary in text box. + ContactType resultContactType = contact instanceof Client ? ContactType.VENDOR + : ContactType.VENDOR; + EventsCenter.getInstance().post(new PersonPanelSelectionChangedEvent(contact)); + + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredContactList().size(), + resultContactType)); + } + + /** + * Utility function to check if a {@code vendor} can fulfil a particular {@code serviceRequired}. + * + * @param vendor The service provider. + * @param serviceRequired The service required. + * @return Returns {@code true} if the service provider can fulfil the service, otherwise {@code false}. + */ + private static boolean vendorCanFulfilService(Vendor vendor, Service serviceRequired) { + return vendor + .getServices() + .values() + .stream() + .filter(serviceProvided -> serviceRequired.isSameServiceTypeAs(serviceProvided)) + .filter(serviceProvided -> serviceRequired.getCost().compareTo(serviceProvided.getCost()) >= 0) + .count() > 0; + } + + /** + * Utility function to get a {@code Collection} of {@code Service} that can be fulfilled by the + * {@code vendor}. + * @param vendor The service provider. + * @param servicesRequired The services required. + * @return Returns the {@code Collection}. + */ + private static Collection servicesCanBeFulfilledByVendor(Vendor vendor, + Collection servicesRequired) { + return servicesRequired + .stream() + .filter(serviceRequired -> vendorCanFulfilService(vendor, serviceRequired)) + .collect(Collectors.toList()); + } + + /** + * Utility function to check if a {@code client} requires and can afford a particular {@code serviceOffered}. + * + * @param client The client. + * @param serviceOffered The service offered. + * @return Returns true if the client requires the service and can afford it. + */ + private static boolean clientWillPayForService(Client client, Service serviceOffered) { + return client + .getServices() + .values() + .stream() + .filter(serviceRequired -> serviceRequired.isSameServiceTypeAs(serviceOffered)) + .filter(serviceRequired -> serviceRequired.getCost().compareTo(serviceOffered.getCost()) >= 0) + .count() > 0; + } + + /** + * Utility function to get a {@code Collection} of {@code Service} needs to be fulfilled by a {@code client} given + * the list of {@code Service} available. + * @param client The client. + * @param servicesProvided The services provided. + * @return Returns the {@code Collection}. + */ + private static Collection servicesNeededToBeFulfilledByClient(Client client, Collection + servicesProvided) { + return servicesProvided + .stream() + .filter(serviceProvided -> clientWillPayForService(client, serviceProvided)) + .collect(Collectors.toList()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AutoMatchCommand // instanceof handles nulls + && contactType.equals(((AutoMatchCommand) other).contactType) + && contactId == ((AutoMatchCommand) other).contactId); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 1f85bcfe85a8..9218885af69f 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -2,7 +2,10 @@ import static java.util.Objects.requireNonNull; +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.DeselectRequestEvent; import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.LackOfPrivilegeException; import seedu.address.model.AddressBook; import seedu.address.model.Model; @@ -12,14 +15,20 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_SUCCESS = "Heart² has been cleared!"; @Override - public CommandResult execute(Model model, CommandHistory history) { + public CommandResult execute(Model model, CommandHistory history) throws LackOfPrivilegeException { requireNonNull(model); + + if (!model.getUserAccount().hasDeletePrivilege()) { + throw new LackOfPrivilegeException(COMMAND_WORD); + } + model.resetData(new AddressBook()); model.commitAddressBook(); + EventsCenter.getInstance().post(new DeselectRequestEvent()); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java index 34e99d786ec6..b1916eea3ac5 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/address/logic/commands/Command.java @@ -2,6 +2,7 @@ import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.commands.exceptions.LackOfPrivilegeException; import seedu.address.model.Model; /** @@ -16,7 +17,9 @@ public abstract class Command { * @param history {@code CommandHistory} which the command should operate on. * @return feedback message of the operation result for display * @throws CommandException If an error occurs during command execution. + * @throws LackOfPrivilegeException If user account does not possess the required privilege + * to execute that command. */ - public abstract CommandResult execute(Model model, CommandHistory history) throws CommandException; - + public abstract CommandResult execute(Model model, CommandHistory history) throws CommandException, + LackOfPrivilegeException; } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index a20e9d49eac7..705abbf88bbd 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -4,52 +4,84 @@ import java.util.List; +import seedu.address.commons.core.EventsCenter; import seedu.address.commons.core.Messages; import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.DeselectRequestEvent; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.commands.exceptions.LackOfPrivilegeException; +import seedu.address.model.ContactType; import seedu.address.model.Model; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes a client 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 COMMAND_WORD_GENERAL = "%1$s%2$s delete"; + /* + the below are necessary for switch statements as a constant expression is required for to compile switch + statements + */ + public static final String COMMAND_WORD_CLIENT = "client delete"; + public static final String COMMAND_WORD_VENDOR = "vendor 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_USAGE = COMMAND_WORD_GENERAL + + ": Deletes the %1$s identified by the assigned unique %1$s ID.\n" + + "Parameters: # (must be a positive integer)\n" + + "Example: " + String.format(COMMAND_WORD_GENERAL, "%1$s", "#3"); - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted %1$s: %2$s"; - private final Index targetIndex; + private final Index id; + private final ContactType contactType; - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; + public DeleteCommand(Index id, ContactType contactType) { + this.id = id; + this.contactType = contactType; } @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { + public CommandResult execute(Model model, CommandHistory history) throws CommandException, + LackOfPrivilegeException { requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + if (!model.getUserAccount().hasDeletePrivilege()) { + throw new LackOfPrivilegeException(String.format(COMMAND_WORD_GENERAL, contactType, "#")); } - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); + model.updateFilteredContactList(contactType.getFilter() + .and(contact -> contact.getId() == id.getOneBased())); + + List filteredList = model.getFilteredContactList(); + + if (filteredList.size() == 0) { + // filtered list size is 0, meaning there is no such contact + model.updateFilteredContactList(contactType.getFilter()); + throw new CommandException(String.format(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, + id.getOneBased())); + } + + if (filteredList.size() > 1) { + throw new RuntimeException("ID is not unique!"); + } + + // filtered list size is 1 (unique ID for client/vendor) + Contact contactToDelete = filteredList.get(0); + model.deleteContact(contactToDelete); + model.updateFilteredContactList(contactType.getFilter()); model.commitAddressBook(); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); + EventsCenter.getInstance().post(new DeselectRequestEvent()); + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, contactToDelete.getType(), + contactToDelete)); } @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 + && id.equals(((DeleteCommand) other).id)); // state check } } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java deleted file mode 100644 index dc782d8e230f..000000000000 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ /dev/null @@ -1,228 +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 static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Edits the details of an existing person in the address book. - */ -public class EditCommand extends Command { - - public static final String COMMAND_WORD = "edit"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; - - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; - public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - - private final Index index; - private final EditPersonDescriptor editPersonDescriptor; - - /** - * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with - */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { - requireNonNull(index); - requireNonNull(editPersonDescriptor); - - this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); - } - - @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.updatePerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - model.commitAddressBook(); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); - } - - /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. - */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; - - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditCommand)) { - return false; - } - - // state check - EditCommand e = (EditCommand) other; - return index.equals(e.index) - && editPersonDescriptor.equals(e.editPersonDescriptor); - } - - /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. - */ - public static class EditPersonDescriptor { - private Name name; - private Phone phone; - private Email email; - private Address address; - private Set tags; - - public EditPersonDescriptor() {} - - /** - * Copy constructor. - * A defensive copy of {@code tags} is used internally. - */ - public EditPersonDescriptor(EditPersonDescriptor toCopy) { - setName(toCopy.name); - setPhone(toCopy.phone); - setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); - } - - /** - * Returns true if at least one field is edited. - */ - public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); - } - - public void setName(Name name) { - this.name = name; - } - - public Optional getName() { - return Optional.ofNullable(name); - } - - public void setPhone(Phone phone) { - this.phone = phone; - } - - public Optional getPhone() { - return Optional.ofNullable(phone); - } - - public void setEmail(Email email) { - this.email = email; - } - - public Optional getEmail() { - return Optional.ofNullable(email); - } - - public void setAddress(Address address) { - this.address = address; - } - - public Optional
getAddress() { - return Optional.ofNullable(address); - } - - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; - } - - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { - return false; - } - - // state check - EditPersonDescriptor e = (EditPersonDescriptor) other; - - return getName().equals(e.getName()) - && getPhone().equals(e.getPhone()) - && getEmail().equals(e.getEmail()) - && getAddress().equals(e.getAddress()) - && getTags().equals(e.getTags()); - } - } -} diff --git a/src/main/java/seedu/address/logic/commands/EditPasswordCommand.java b/src/main/java/seedu/address/logic/commands/EditPasswordCommand.java new file mode 100644 index 000000000000..92c952cbdf22 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditPasswordCommand.java @@ -0,0 +1,111 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.FileNotFoundException; +import java.nio.file.Path; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.DeselectRequestEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.security.PasswordAuthentication; +import seedu.address.model.Model; +import seedu.address.model.account.Account; +import seedu.address.storage.AccountStorage; +import seedu.address.storage.XmlAccountStorage; + +/** + * Edit the password of the user. + */ +public class EditPasswordCommand extends Command { + public static final String COMMAND_WORD = "change password"; + public static final Prefix PREFIX_OLDPASSWORD = new Prefix("o/"); + public static final Prefix PREFIX_NEWPASSWORD = new Prefix("n/"); + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edit your current account password. " + + "Parameters: " + + PREFIX_OLDPASSWORD + "OLD_PASSWORD " + + PREFIX_NEWPASSWORD + "NEW_PASSWORD " + + "\nExample: " + COMMAND_WORD + " " + + PREFIX_OLDPASSWORD + "thisIsMyOldPassw0rd " + + PREFIX_NEWPASSWORD + "thisIsMyNewPassw0rd"; + + public static final String MESSAGE_SUCCESS = "Password changed successfully."; + public static final String MESSAGE_FAILURE_PASSWORDWRONG = "Failed to change your password. " + + "Old password was not correct."; + public static final String MESSAGE_FAILURE_FILENOTFOUND = "Failed to change your password. Unable to find" + + "your current account file path."; + public static final String MESSAGE_FAILURE_SAMEPASSWORD = "Failed to change your password. " + + "Your old password and new password is the same."; + public static final String MESSAGE_FAILURE = "Failed to change your password."; + public static final String MESSAGE_FAILURE_EMPTYOLDPASSWORD = "Old password cannot be empty." + + " Please try again with your correct old password.";; + public static final String MESSAGE_FAILURE_OLDPASSWORDWITHSPACE = "Old password cannot contain space." + + " Please try again with your correct old password."; + public static final String MESSAGE_FAILURE_EMPTYNEWPASSWORD = "New Password cannot be empty." + + " Please try again with another new password.";; + public static final String MESSAGE_FAILURE_NEWPASSWORDWITHSPACE = "New Password cannot contain space." + + " Please try again with another new password."; + + private String userTypedOldPassword; + private String userTypedNewPassword; + private Path accountListPath; + + public EditPasswordCommand(String userTypedOldPassword, String userTypedNewPassword) { + requireNonNull(userTypedOldPassword); + requireNonNull(userTypedNewPassword); + this.userTypedOldPassword = userTypedOldPassword; + this.userTypedNewPassword = userTypedNewPassword; + this.accountListPath = null; + } + + public EditPasswordCommand(String userTypedOldPassword, String userTypedNewPassword, Path accountListPath) { + requireNonNull(userTypedOldPassword); + requireNonNull(userTypedNewPassword); + this.userTypedOldPassword = userTypedOldPassword; + this.userTypedNewPassword = userTypedNewPassword; + this.accountListPath = accountListPath; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + Account currentAccount = model.getUserAccount(); + String username = currentAccount.getUserName(); + String actualOldPassword = currentAccount.getPassword(); + + PasswordAuthentication passwordAuthentication = new PasswordAuthentication(); + + if (!passwordAuthentication.authenticate(userTypedOldPassword.toCharArray(), actualOldPassword)) { + throw new CommandException(MESSAGE_FAILURE_PASSWORDWRONG); + } + + if (userTypedOldPassword.equals(userTypedNewPassword)) { + throw new CommandException(MESSAGE_FAILURE_SAMEPASSWORD); + } + + AccountStorage accountStorage = accountListPath == null + ? new XmlAccountStorage() + : new XmlAccountStorage(accountListPath); + + try { + String hashedNewPassword = PasswordAuthentication.getHashedPasswordFromPlainText(userTypedNewPassword); + + accountStorage.updateAccountPassword(username, hashedNewPassword); + model.commiteUserChangedPasswordSuccessfully(hashedNewPassword); + EventsCenter.getInstance().post(new DeselectRequestEvent()); + return new CommandResult(MESSAGE_SUCCESS); + } catch (FileNotFoundException e) { + throw new CommandException(MESSAGE_FAILURE_FILENOTFOUND); + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EditPasswordCommand // instanceof handles nulls + && userTypedOldPassword.equals(((EditPasswordCommand) other).userTypedOldPassword) + && userTypedNewPassword.equals(((EditPasswordCommand) other).userTypedNewPassword)); + } +} 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 beb178e3a3f5..000000000000 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ /dev/null @@ -1,43 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.core.Messages; -import seedu.address.logic.CommandHistory; -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, CommandHistory history) { - requireNonNull(model); - model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof FindCommand // instanceof handles nulls - && predicate.equals(((FindCommand) other).predicate)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 6d44824c7d1b..4433f60ee7ef 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -1,25 +1,58 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.events.ui.DeselectRequestEvent; import seedu.address.logic.CommandHistory; +import seedu.address.model.ContactType; import seedu.address.model.Model; +import seedu.address.model.contact.ContactContainsKeywordsPredicate; /** - * Lists all persons in the address book to the user. + * Finds and lists all contacts in address book which contain all of the argument keywords. + * Keyword matching is case insensitive. */ public class ListCommand extends Command { - public static final String COMMAND_WORD = "list"; + public static final String COMMAND_WORD_GENERAL = "%1$s list"; + public static final String COMMAND_WORD_CLIENT = "client list"; + public static final String COMMAND_WORD_VENDOR = "vendor list"; - public static final String MESSAGE_SUCCESS = "Listed all persons"; + public static final String MESSAGE_USAGE = COMMAND_WORD_GENERAL + ": Lists all %1$ss which contain all of " + + "the specified keywords (case-insensitive) and displays them as a list with their IDs.\n" + + "Parameters: n/[KEYWORD] p/[KEYWORD] e/[KEYWORD] a/[KEYWORD] t/[KEYWORD] ...\n" + + "Example: " + COMMAND_WORD_GENERAL + " n/alice bob charlie"; + private final ContactContainsKeywordsPredicate predicate; + private final ContactType contactType; + + public ListCommand(ContactContainsKeywordsPredicate predicate, ContactType contactType) { + this.predicate = predicate; + this.contactType = contactType; + } @Override public CommandResult execute(Model model, CommandHistory history) { requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); + EventsCenter.getInstance().post(new DeselectRequestEvent()); + + if (predicate.equals(new ContactContainsKeywordsPredicate())) { + model.updateFilteredContactList(contactType.getFilter()); + return new CommandResult(String.format(Messages.MESSAGE_LIST_ALL_X, contactType)); + } + + model.updateFilteredContactList(contactType.getFilter().and(predicate)); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredContactList().size(), + contactType)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ListCommand // instanceof handles nulls + && predicate.equals(((ListCommand) other).predicate)); // state check } } diff --git a/src/main/java/seedu/address/logic/commands/LoginCommand.java b/src/main/java/seedu/address/logic/commands/LoginCommand.java new file mode 100644 index 000000000000..ef37beeb0d16 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LoginCommand.java @@ -0,0 +1,104 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.logging.Logger; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.DeselectRequestEvent; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.Prefix; +import seedu.address.model.Model; +import seedu.address.model.UserPrefs; +import seedu.address.model.account.Account; +import seedu.address.model.account.AccountList; +import seedu.address.model.account.Role; +import seedu.address.storage.AccountStorage; +import seedu.address.storage.XmlAccountStorage; + +/** + * Log user in with his username and password to gain admin access to the system. + */ +public class LoginCommand extends Command { + public static final String COMMAND_WORD = "login"; + public static final Prefix PREFIX_USERNNAME = new Prefix("u/"); + public static final Prefix PREFIX_PASSWORD = new Prefix("p/"); + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Log in with your username and password. " + + "Parameters: " + + PREFIX_USERNNAME + "USERNAME " + + PREFIX_PASSWORD + "PASSWORD " + + "\nExample: " + COMMAND_WORD + " " + + PREFIX_USERNNAME + "heartsquare " + + PREFIX_PASSWORD + "H3artSquar3"; + + public static final String MESSAGE_SUCCESS = "Successfully logged in."; + public static final String MESSAGE_FAILURE = "Login failed. Please check your username or password and try again."; + private static final Logger logger = LogsCenter.getLogger(LoginCommand.class); + + private String username; + private String password; + private Path accountListPath; + + /** + * Create a new LoginCommand with default path to AccountList. + */ + public LoginCommand(String username, String password) { + requireNonNull(username); + requireNonNull(password); + this.username = username; + this.password = password; + this.accountListPath = null; + } + + /** + * Create a new Login Command with the path to AccountList specified. + */ + public LoginCommand(String username, String password, Path accountListPath) { + requireNonNull(username); + requireNonNull(password); + this.username = username; + this.password = password; + this.accountListPath = accountListPath; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + AccountStorage accountStorage = accountListPath == null + ? new XmlAccountStorage() + : new XmlAccountStorage(accountListPath); + + try { + AccountList accountList = accountStorage.getAccountList(); + + if (accountList.hasUsernameAndPassword(username, password)) { + Role userAccountRole = accountList.getAccountRole(username); + Account accountToCommit = new Account(username, password, userAccountRole); + accountToCommit.transformToHashedAccount(); + model.commitUserLoggedInSuccessfully(accountToCommit); + EventsCenter.getInstance().post(new DeselectRequestEvent()); + UserPrefs.setUsernameAndRoleToDisplay(username + " / " + userAccountRole.getRole()); + return new CommandResult(MESSAGE_SUCCESS); + } + } catch (DataConversionException e) { + logger.warning("Data file not in the correct format"); + } catch (IOException e2) { + logger.warning("Problem while reading from the file containing all the accounts"); + } + + throw new CommandException(MESSAGE_FAILURE); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof LoginCommand // instanceof handles nulls + && username.equals(((LoginCommand) other).username) + && password.equals(((LoginCommand) other).password)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/LogoutCommand.java b/src/main/java/seedu/address/logic/commands/LogoutCommand.java new file mode 100644 index 000000000000..8a8807f9baff --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LogoutCommand.java @@ -0,0 +1,25 @@ +package seedu.address.logic.commands; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.LogoutRequestEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.model.Model; + +/** + * Log the user out of the application. + */ +public class LogoutCommand extends Command { + public static final String COMMAND_WORD = "logout"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Log out of the application. " + + "\nExample: " + COMMAND_WORD; + + public static final String MESSAGE_SUCCESS = "Successfully logged out."; + + @Override + public CommandResult execute(Model model, CommandHistory history) { + model.commitUserLoggedOutSuccessfully(); + EventsCenter.getInstance().post(new LogoutRequestEvent()); + history.clear(); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java index 227771a4eef6..0477f1c12d57 100644 --- a/src/main/java/seedu/address/logic/commands/RedoCommand.java +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -1,11 +1,18 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.DeselectRequestEvent; +import seedu.address.commons.util.CollectionUtil; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.ContactType; import seedu.address.model.Model; +import seedu.address.model.contact.Contact; /** * Reverts the {@code model}'s address book to its previously undone state. @@ -24,8 +31,14 @@ public CommandResult execute(Model model, CommandHistory history) throws Command throw new CommandException(MESSAGE_FAILURE); } + List listBeforeRedo = new ArrayList<>(model.getAddressBook().getContactList()); model.redoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + List listAfterRedo = model.getAddressBook().getContactList(); + + ContactType toFilter = CollectionUtil.compareListOfContacts(listAfterRedo, listBeforeRedo); + + model.updateFilteredContactList(toFilter.getFilter()); + EventsCenter.getInstance().post(new DeselectRequestEvent()); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/RegisterAccountCommand.java b/src/main/java/seedu/address/logic/commands/RegisterAccountCommand.java new file mode 100644 index 000000000000..de2a68bfe47c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RegisterAccountCommand.java @@ -0,0 +1,109 @@ +package seedu.address.logic.commands; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.commands.exceptions.LackOfPrivilegeException; +import seedu.address.logic.parser.Prefix; +import seedu.address.model.Model; +import seedu.address.model.account.Account; +import seedu.address.storage.AccountStorage; +import seedu.address.storage.XmlAccountStorage; + +/** + * Register a new account and save the account into database. Only a SUPER_USER account + * can register a new account. + */ +public class RegisterAccountCommand extends Command { + public static final String COMMAND_WORD = "register account"; + public static final Prefix PREFIX_USERNNAME = new Prefix("u/"); + public static final Prefix PREFIX_PASSWORD = new Prefix("p/"); + public static final Prefix PREFIX_ROLE = new Prefix("r/"); + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Register a new account. " + + "Parameters: " + + PREFIX_USERNNAME + "USERNAME " + + PREFIX_PASSWORD + "PASSWORD " + + PREFIX_ROLE + "ROLE " + + "\nExample: " + COMMAND_WORD + " " + + PREFIX_USERNNAME + "newUserName " + + PREFIX_PASSWORD + "newPassword " + + PREFIX_ROLE + "superuser "; + + public static final String MESSAGE_SUCCESS = "Successfully registered the account."; + public static final String MESSAGE_FAILURE = "Failed to register the new account. " + + "Please make sure to use only \"r/superuser\" " + + "or r/readonlyuser\" for role."; + public static final String MESSAGE_FAILURE_FILENOTFOUND = "Failed to find the file to save account to."; + public static final String MESSAGE_FAILURE_EMPTYUSERNAME = "Username cannot be empty." + + " Please try again with another username."; + public static final String MESSAGE_FAILURE_USERNAMEWITHSPACE = "Username cannot contain spaces." + + " Please try again with another username."; + public static final String MESSAGE_FAILURE_EMPTYPASSWORD = "Password cannot be empty." + + " Please try again with another password."; + public static final String MESSAGE_FAILURE_PASSWORDWITHSPACE = "Password cannot contain spaces." + + " Please try again with another password."; + public static final String MESSAGE_REGISTERACCOUNT_INVOKEATLOGIN = "You can only register an account after " + + "logging in. Please contact your admin to get an account."; + public static final String MESSAGE_FAILURE_DUPLICATE = "Username is taken. Please try again with another username."; + public static final String MESSAGE_INVALIDROLE = "Role should contains only" + + "\"superuser\" or \"readonlyuser\"."; + + private Account account; + private Path accountListPath; + + /** + * Creates a new RegisterAccountCommand with the account, and save the account to the default path. + * @param account The account to be registered. + */ + public RegisterAccountCommand(Account account) { + this.account = account; + this.accountListPath = null; + } + + /** + * Create a new RegisterAccountCommand with the account, and save to the specified path. + * @param account The account to be registered + * @param accountListPath The Path to save the account to. + */ + public RegisterAccountCommand(Account account, Path accountListPath) { + this.account = account; + this.accountListPath = accountListPath; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException, + LackOfPrivilegeException { + if (!model.getUserAccount().hasAccountCreationPrivilege()) { + throw new LackOfPrivilegeException(COMMAND_WORD); + } + + AccountStorage accountStorage = accountListPath == null + ? new XmlAccountStorage() + : new XmlAccountStorage(accountListPath); + + try { + if (accountStorage.getAccountList().hasUserName(account.getUserName())) { + throw new CommandException(MESSAGE_FAILURE_DUPLICATE); + } + accountStorage.saveAccount(account); + return new CommandResult(MESSAGE_SUCCESS); + } catch (IOException | DataConversionException e) { + if (e instanceof FileNotFoundException) { + throw new CommandException(MESSAGE_FAILURE_FILENOTFOUND); + } + throw new CommandException(MESSAGE_FAILURE); + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof RegisterAccountCommand // instanceof handles nulls + && account.equals(((RegisterAccountCommand) other).account)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SelectCommand.java b/src/main/java/seedu/address/logic/commands/SelectCommand.java index f5e8c1a8722e..8299e48cc03d 100644 --- a/src/main/java/seedu/address/logic/commands/SelectCommand.java +++ b/src/main/java/seedu/address/logic/commands/SelectCommand.java @@ -7,51 +7,71 @@ import seedu.address.commons.core.EventsCenter; import seedu.address.commons.core.Messages; import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.ClearBrowserPanelRequestEvent; import seedu.address.commons.events.ui.JumpToListRequestEvent; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.ContactType; import seedu.address.model.Model; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** - * Selects a person identified using it's displayed index from the address book. + * Selects a client identified using it's displayed index from the address book. */ public class SelectCommand extends Command { - public static final String COMMAND_WORD = "select"; + public static final String COMMAND_WORD_GENERAL = "%1$s%2$s view"; + public static final String COMMAND_WORD_CLIENT = "client view"; + public static final String COMMAND_WORD_VENDOR = "vendor view"; - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Selects 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_USAGE = COMMAND_WORD_GENERAL + + ": Selects the %1$s identified by the assigned unique %1$s ID to view.\n" + + "Parameters: # (must be a positive integer)\n" + + "Example: " + String.format(COMMAND_WORD_GENERAL, "%1$s", "#3"); - public static final String MESSAGE_SELECT_PERSON_SUCCESS = "Selected Person: %1$s"; + public static final String MESSAGE_SELECT_CONTACT_SUCCESS = "Viewing %1$s#%2$s"; - private final Index targetIndex; + private final Index id; + private final ContactType contactType; - public SelectCommand(Index targetIndex) { - this.targetIndex = targetIndex; + public SelectCommand(Index id, ContactType contactType) { + this.id = id; + this.contactType = contactType; } @Override public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); - List filteredPersonList = model.getFilteredPersonList(); + model.updateFilteredContactList(contactType.getFilter() + .and(contact -> contact.getId() == id.getOneBased())); - if (targetIndex.getZeroBased() >= filteredPersonList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + List filteredList = model.getFilteredContactList(); + + if (filteredList.size() == 0) { + // filtered list size is 0, meaning there is no such contact + model.updateFilteredContactList(contactType.getFilter()); + EventsCenter.getInstance().post(new ClearBrowserPanelRequestEvent()); + throw new CommandException(String.format(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, + id.getOneBased())); } - EventsCenter.getInstance().post(new JumpToListRequestEvent(targetIndex)); - return new CommandResult(String.format(MESSAGE_SELECT_PERSON_SUCCESS, targetIndex.getOneBased())); + if (filteredList.size() > 1) { + throw new RuntimeException("ID is not unique!"); + } + // filtered list size is 1 (unique ID for client/vendor) + Contact contactToSelect = filteredList.get(0); + model.updateFilteredContactList(contactType.getFilter()); + EventsCenter.getInstance().post(new JumpToListRequestEvent(Index.fromZeroBased(model.getFilteredContactList() + .indexOf(contactToSelect)))); + return new CommandResult((String.format(MESSAGE_SELECT_CONTACT_SUCCESS, contactType, id.getOneBased()))); } @Override public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof SelectCommand // instanceof handles nulls - && targetIndex.equals(((SelectCommand) other).targetIndex)); // state check + && id.equals(((SelectCommand) other).id)); // state check } } diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java index 40441264f346..ec6b554ff464 100644 --- a/src/main/java/seedu/address/logic/commands/UndoCommand.java +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -1,11 +1,18 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.DeselectRequestEvent; +import seedu.address.commons.util.CollectionUtil; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.ContactType; import seedu.address.model.Model; +import seedu.address.model.contact.Contact; /** * Reverts the {@code model}'s address book to its previous state. @@ -24,8 +31,15 @@ public CommandResult execute(Model model, CommandHistory history) throws Command throw new CommandException(MESSAGE_FAILURE); } + List listBeforeUndo = new ArrayList<>(model.getAddressBook().getContactList()); model.undoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + List listAfterUndo = model.getAddressBook().getContactList(); + + ContactType toFilter = CollectionUtil.compareListOfContacts(listAfterUndo, listBeforeUndo); + + model.updateFilteredContactList(toFilter.getFilter()); + EventsCenter.getInstance().post(new DeselectRequestEvent()); + return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/UpdateCommand.java b/src/main/java/seedu/address/logic/commands/UpdateCommand.java new file mode 100644 index 000000000000..3a015920bfc8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UpdateCommand.java @@ -0,0 +1,308 @@ +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 java.util.Collections; +import java.util.HashMap; +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.EventsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.DeselectRequestEvent; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.commands.exceptions.LackOfPrivilegeException; +import seedu.address.model.ContactType; +import seedu.address.model.Model; +import seedu.address.model.client.Client; +import seedu.address.model.contact.Address; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Service; +import seedu.address.model.tag.Tag; +import seedu.address.model.vendor.Vendor; + +/** + * Edits the details of an existing contact in the address book. + */ +public class UpdateCommand extends Command { + + + public static final String COMMAND_WORD = "update"; + public static final String COMMAND_WORD_GENERAL = "%1$s%2$s update"; + public static final String COMMAND_WORD_CLIENT = "client update"; + public static final String COMMAND_WORD_VENDOR = "vendor update"; + + public static final String MESSAGE_USAGE = COMMAND_WORD_GENERAL + ": Updates the details of the %1$s identified " + + "by the assigned unique %1$s ID.\n" + + "Existing values will be overwritten by the input values.\n" + + "Parameters: # (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_PHONE + "PHONE] " + + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + String.format(COMMAND_WORD_GENERAL, "%1$s", "#3") + + PREFIX_PHONE + "91234567 " + + PREFIX_EMAIL + "johndoe@example.com"; + + public static final String MESSAGE_EDIT_CONTACT_SUCCESS = "Updated %1$s: %2$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to update must be provided."; + public static final String MESSAGE_DUPLICATE_CONTACT = "This contact already exists in the address book." + + "\nYou cannot add a contact with the same name, and either the same phone number" + + " or the same email address."; + + private final Index id; + private final EditContactDescriptor editContactDescriptor; + private final ContactType contactType; + + /** + * @param id of the contact in the filtered contact list to edit + * @param editContactDescriptor details to edit the contact with + * @param contactType + */ + public UpdateCommand(Index id, EditContactDescriptor editContactDescriptor, ContactType contactType) { + requireNonNull(id); + requireNonNull(editContactDescriptor); + + this.id = id; + this.editContactDescriptor = new EditContactDescriptor(editContactDescriptor); + this.contactType = contactType; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException, + LackOfPrivilegeException { + requireNonNull(model); + + if (!model.getUserAccount().hasWritePrivilege()) { + throw new LackOfPrivilegeException(String.format(COMMAND_WORD_GENERAL, contactType, "#")); + } + + // id is unique + model.updateFilteredContactList(contactType.getFilter().and(contact -> contact.getId() == id.getOneBased())); + + List filteredList = model.getFilteredContactList(); + + if (filteredList.size() == 0) { + // filtered list size is 0, meaning there is no such contact + model.updateFilteredContactList(contactType.getFilter()); + throw new CommandException(String.format(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, + id.getOneBased())); + } + + if (filteredList.size() > 1) { + throw new RuntimeException("ID is not unique!"); + } + + Contact contactToEdit = filteredList.get(0); + Contact editedContact = createEditedContact(contactToEdit, editContactDescriptor, contactType); + + List fullList = model.getAddressBook().getContactList(); + for (Contact c: fullList) { + if (c.isSameContact(editedContact) && c.getId() != id.getOneBased()) { + model.updateFilteredContactList(contactType.getFilter()); + throw new CommandException(MESSAGE_DUPLICATE_CONTACT); + } + } + + model.updateContact(contactToEdit, editedContact); + model.updateFilteredContactList(contactType.getFilter()); + model.commitAddressBook(); + EventsCenter.getInstance().post(new DeselectRequestEvent()); + return new CommandResult(String.format(MESSAGE_EDIT_CONTACT_SUCCESS, contactToEdit.getType(), editedContact)); + } + + /** + * Creates and returns a { + * @code Contact} with the details of {@code contactToEdit} + * edited with {@code editContactDescriptor}. + */ + public static Contact createEditedContact(Contact contactToEdit, EditContactDescriptor editContactDescriptor, + ContactType contactType) { + assert contactToEdit != null; + + Name updatedName = editContactDescriptor.getName().orElse(contactToEdit.getName()); + Phone updatedPhone = editContactDescriptor.getPhone().orElse(contactToEdit.getPhone()); + Email updatedEmail = editContactDescriptor.getEmail().orElse(contactToEdit.getEmail()); + Address updatedAddress = editContactDescriptor.getAddress().orElse(contactToEdit.getAddress()); + Set updatedTags = editContactDescriptor.getTags().orElse(contactToEdit.getTags()); + Map updatedServices = editContactDescriptor.getServices().orElse(contactToEdit.getServices()); + int id = contactToEdit.getId(); + + switch (contactType) { + case CLIENT: + return new Client(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, + updatedServices, id); + case VENDOR: + return new Vendor(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, + updatedServices, id); + default: + // should nvr come in here + throw new RuntimeException("No such Contact Type!"); + } + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof UpdateCommand)) { + return false; + } + + // state check + UpdateCommand e = (UpdateCommand) other; + return id.equals(e.id) + && editContactDescriptor.equals(e.editContactDescriptor); + } + + /** + * Stores the details to edit the contact with. Each non-empty field value will replace the + * corresponding field value of the contact. + */ + public static class EditContactDescriptor { + private Name name; + private Phone phone; + private Email email; + private Address address; + private Set tags; + private Map services; + + public EditContactDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditContactDescriptor(EditContactDescriptor toCopy) { + setName(toCopy.name); + setPhone(toCopy.phone); + setEmail(toCopy.email); + setAddress(toCopy.address); + setTags(toCopy.tags); + setServices(toCopy.services); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setPhone(Phone phone) { + this.phone = phone; + } + + public Optional getPhone() { + return Optional.ofNullable(phone); + } + + public void setEmail(Email email) { + this.email = email; + } + + public Optional getEmail() { + return Optional.ofNullable(email); + } + + public void setAddress(Address address) { + this.address = address; + } + + public Optional
getAddress() { + return Optional.ofNullable(address); + } + + /** + * Sets {@code tags} to this object's {@code tags}. + * A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + + /** + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Optional> getTags() { + return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + } + + /** + * Sets {@code services} to this object's {@code services}. + * A defensive copy of {@code services} is used internally. + */ + public void setServices(Map services) { + this.services = (services != null) ? new HashMap<>(services) : null; + } + + /** + * Returns an unmodifiable services map, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code services} is null. + */ + public Optional> getServices() { + return (services != null) ? Optional.of(Collections.unmodifiableMap(services)) : Optional.empty(); + } + + /** + * Adds the specified service. + * @param service Service to be added. + */ + public void addService(Service service) { + this.services.put(service.getName(), service); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditContactDescriptor)) { + return false; + } + + // state check + EditContactDescriptor e = (EditContactDescriptor) other; + + return getName().equals(e.getName()) + && getPhone().equals(e.getPhone()) + && getEmail().equals(e.getEmail()) + && getAddress().equals(e.getAddress()) + && getTags().equals(e.getTags()) + && getServices().equals(e.getServices()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/LackOfPrivilegeException.java b/src/main/java/seedu/address/logic/commands/exceptions/LackOfPrivilegeException.java new file mode 100644 index 000000000000..e970e32fbde1 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/exceptions/LackOfPrivilegeException.java @@ -0,0 +1,12 @@ +package seedu.address.logic.commands.exceptions; + +/** + * Represents an error when a user whose account does not have certain privileges tries to + * execute certain commands meant for account with those privileges. + * See {@link seedu.address.model.account.Role}. + */ +public class LackOfPrivilegeException extends Exception { + public LackOfPrivilegeException(String commandName) { + super("You do not have privilege to access \'" + commandName + "\' command."); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 3b8bfa035e83..ce0c00c1dcb3 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -12,31 +12,34 @@ 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.ContactType; +import seedu.address.model.client.Client; +import seedu.address.model.contact.Address; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Phone; import seedu.address.model.tag.Tag; +import seedu.address.model.vendor.Vendor; /** * Parses input arguments and creates a new AddCommand object */ public class AddCommandParser implements Parser { + private final ContactType contactType; + + public AddCommandParser(ContactType contactType) { + this.contactType = contactType; + } + /** * 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)); - } + ArgumentMultimap argMultimap = createLegalArgumentMultimap(args); Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); @@ -44,9 +47,42 @@ public AddCommand parse(String args) throws ParseException { 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(createContact(name, phone, email, address, tagList)); + } + + /** + * Creates either a client or a service provider contact given the name, phone, email, address and tags. + */ + private Contact createContact(Name name, Phone phone, Email email, Address address, Set tagList) { + switch (contactType) { + case CLIENT: + return new Client(name, phone, email, address, tagList); + case VENDOR: + return new Vendor(name, phone, email, address, tagList); + default: + // should nvr come here + throw new RuntimeException("No such Contact Type"); + } + } + + /** + * Creates a {@code ArgumentMultimap} using the arguments from the input. + * @param args The arguments from the input + * @return the {@code ArgumentMultimap} generated using the input arguments. + * @throws ParseException If a legal {@code ArgumentMultimap} is not able to be created due to an invalid command + * format. + */ + protected ArgumentMultimap createLegalArgumentMultimap(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, + String.format(AddCommand.MESSAGE_USAGE, contactType))); + } - return new AddCommand(person); + return argMultimap; } /** diff --git a/src/main/java/seedu/address/logic/parser/AddServiceCommandParser.java b/src/main/java/seedu/address/logic/parser/AddServiceCommandParser.java new file mode 100644 index 000000000000..e798e5709930 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddServiceCommandParser.java @@ -0,0 +1,80 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SERVICE; + +import java.util.List; +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddServiceCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.ContactType; +import seedu.address.model.contact.Service; + +/** + * Parses input arguments and creates a new AddServiceCommand object + */ +public class AddServiceCommandParser implements Parser { + private final ContactType contactType; + + public AddServiceCommandParser(ContactType contactType) { + this.contactType = contactType; + } + + /** + * Parses the given {@code String} of arguments in the context of the AddServiceCommand + * and returns an AddServiceCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public AddServiceCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SERVICE, PREFIX_COST); + Index id; + try { + id = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + String.format(AddServiceCommand.MESSAGE_USAGE, contactType, "#"))); + } + + if (!arePrefixesPresent(argMultimap, PREFIX_SERVICE, PREFIX_COST)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + String.format(AddServiceCommand.MESSAGE_USAGE, contactType, "#"))); + } + + List allServiceNames = argMultimap.getAllValues(PREFIX_SERVICE); + List allServiceCost = argMultimap.getAllValues(PREFIX_COST); + + // Guard against missing or extraneous service name/cost + if (allServiceNames.size() != 1 || allServiceCost.size() != 1) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + String.format(AddServiceCommand.MESSAGE_USAGE, contactType, "#"))); + } + + String serviceName = allServiceNames.get(0).toLowerCase(); + String serviceCost = allServiceCost.get(0); + + // Guard against invalid service types + if (!Service.isValidServiceName(serviceName)) { + throw new ParseException(Service.MESSAGE_SERVICE_NAME_CONSTRAINTS); + } + if (!Service.isValidServiceCost(serviceCost)) { + throw new ParseException(Service.MESSAGE_SERVICE_COST_CONSTRAINTS); + } + + Service service = new Service(serviceName.toLowerCase(), serviceCost); + return new AddServiceCommand(id, service, contactType); + } + /** + * 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 index b7d57f5db86a..81048d9afbd0 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -7,19 +7,25 @@ import java.util.regex.Pattern; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddServiceCommand; +import seedu.address.logic.commands.AutoMatchCommand; 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.EditPasswordCommand; import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.HistoryCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.LoginCommand; +import seedu.address.logic.commands.LogoutCommand; import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.RegisterAccountCommand; import seedu.address.logic.commands.SelectCommand; import seedu.address.logic.commands.UndoCommand; +import seedu.address.logic.commands.UpdateCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.ContactType; /** * Parses user input. @@ -29,64 +35,243 @@ public class AddressBookParser { /** * Used for initial separation of command word and args. */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + private static final Pattern COMMAND_FORMAT = + Pattern.compile("(?[a-zA-Z]+)(?#[\\d\\w-]*)?(?[\\s]+(?!./)" + + "[a-zA-Z]+)?(?.*)"); /** - * Parses user input into command for execution. + * Parses user input into command for execution. This method is use before user has successfully logged in. + * + * @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 parseCommandBeforeLoggedIn(String userInput) throws ParseException { + final Matcher matcher = COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = getCommandWord(matcher.group("firstWord"), matcher.group("secondWord")); + final String identifier = matcher.group("identifier"); + final String arguments = matcher.group("arguments"); + switch (commandWord) { + + case LoginCommand.COMMAND_WORD: + return new LoginCommandParser().parse(arguments); + + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + + case RegisterAccountCommand.COMMAND_WORD: + requireNullIdentifier(identifier, RegisterAccountCommand.MESSAGE_USAGE); + throw new ParseException(RegisterAccountCommand.MESSAGE_REGISTERACCOUNT_INVOKEATLOGIN); + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + + /** + * Combines the first and second words to form the command word. + */ + private String getCommandWord(String firstWord, String secondWord) { + String commandWord; + + if (secondWord == null) { + commandWord = firstWord; + } else { + commandWord = String.format("%s %s", firstWord, secondWord.trim()); + } + + return commandWord; + } + + /** + * Parses user input into command for execution. This method will only be called after + * user has successfully log in. * * @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()); + final Matcher matcher = 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 firstWord = matcher.group("firstWord"); + final String identifier = matcher.group("identifier"); + final String secondWord = matcher.group("secondWord"); final String arguments = matcher.group("arguments"); + + final String commandWord = getCommandWord(firstWord, secondWord); + switch (commandWord) { - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); + case RegisterAccountCommand.COMMAND_WORD: + requireNullIdentifier(identifier, RegisterAccountCommand.MESSAGE_USAGE); + return new RegisterAccountCommandParser().parse(arguments); + + case LogoutCommand.COMMAND_WORD: + return new LogoutCommand(); - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); + case EditPasswordCommand.COMMAND_WORD: + requireNullIdentifier(identifier, EditPasswordCommand.MESSAGE_USAGE); + return new EditPasswordCommandParser().parse(arguments); - case SelectCommand.COMMAND_WORD: - return new SelectCommandParser().parse(arguments); + case SelectCommand.COMMAND_WORD_CLIENT: + requireEmptyArguments(arguments, ContactType.CLIENT, SelectCommand.MESSAGE_USAGE); + requireIdentifierNonNull(identifier, ContactType.CLIENT, SelectCommand.MESSAGE_USAGE); + return new SelectCommandParser(ContactType.CLIENT).parse(identifier); - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); + case SelectCommand.COMMAND_WORD_VENDOR: + requireEmptyArguments(arguments, ContactType.VENDOR, SelectCommand.MESSAGE_USAGE); + requireIdentifierNonNull(identifier, ContactType.VENDOR, SelectCommand.MESSAGE_USAGE); + return new SelectCommandParser(ContactType.VENDOR).parse(identifier); case ClearCommand.COMMAND_WORD: return new ClearCommand(); - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - case HistoryCommand.COMMAND_WORD: return new HistoryCommand(); case ExitCommand.COMMAND_WORD: return new ExitCommand(); - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - case UndoCommand.COMMAND_WORD: return new UndoCommand(); case RedoCommand.COMMAND_WORD: return new RedoCommand(); + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + case AddCommand.COMMAND_WORD_CLIENT: + requireNullIdentifier(identifier, ContactType.CLIENT, AddCommand.MESSAGE_USAGE); + return new AddCommandParser(ContactType.CLIENT).parse(arguments); + + case DeleteCommand.COMMAND_WORD_CLIENT: + requireEmptyArguments(arguments, ContactType.CLIENT, DeleteCommand.MESSAGE_USAGE); + requireIdentifierNonNull(identifier, ContactType.CLIENT, DeleteCommand.MESSAGE_USAGE); + return new DeleteCommandParser(ContactType.CLIENT).parse(identifier); + + case ListCommand.COMMAND_WORD_CLIENT: + requireNullIdentifier(identifier, ContactType.CLIENT, ListCommand.MESSAGE_USAGE); + return new ListCommandParser(ContactType.CLIENT).parse(arguments); + + case UpdateCommand.COMMAND_WORD_CLIENT: + return new UpdateCommandParser(ContactType.CLIENT) + .parse(String.format("%s %s", requireIdentifierNonNull(identifier, ContactType.CLIENT, + UpdateCommand.MESSAGE_USAGE).substring(1), arguments)); + + case AddServiceCommand.COMMAND_WORD_CLIENT: + return new AddServiceCommandParser(ContactType.CLIENT) + .parse(String.format("%s %s", requireIdentifierNonNull(identifier, ContactType.CLIENT, + AddServiceCommand.MESSAGE_USAGE).substring(1), arguments)); + + case AutoMatchCommand.COMMAND_WORD_CLIENT: + requireEmptyArguments(arguments, ContactType.CLIENT, AutoMatchCommand.MESSAGE_USAGE_CLIENT); + requireIdentifierNonNull(identifier, ContactType.CLIENT, AutoMatchCommand.MESSAGE_USAGE_CLIENT); + return new AutoMatchCommandParser().parse(firstWord + identifier); + + case AutoMatchCommand.COMMAND_WORD_VENDOR: + requireEmptyArguments(arguments, ContactType.VENDOR, AutoMatchCommand.MESSAGE_USAGE_VENDOR); + requireIdentifierNonNull(identifier, ContactType.VENDOR, AutoMatchCommand.MESSAGE_USAGE_VENDOR); + return new AutoMatchCommandParser().parse(firstWord + identifier); + + case AddCommand.COMMAND_WORD_VENDOR: + requireNullIdentifier(identifier, ContactType.VENDOR, AddCommand.MESSAGE_USAGE); + return new AddCommandParser(ContactType.VENDOR).parse(arguments); + + case DeleteCommand.COMMAND_WORD_VENDOR: + requireEmptyArguments(arguments, ContactType.VENDOR, DeleteCommand.MESSAGE_USAGE); + requireIdentifierNonNull(identifier, ContactType.VENDOR, DeleteCommand.MESSAGE_USAGE); + return new DeleteCommandParser(ContactType.VENDOR).parse(identifier); + + case ListCommand.COMMAND_WORD_VENDOR: + requireNullIdentifier(identifier, ContactType.VENDOR, ListCommand.MESSAGE_USAGE); + return new ListCommandParser(ContactType.VENDOR).parse(arguments); + + case UpdateCommand.COMMAND_WORD_VENDOR: + return new UpdateCommandParser(ContactType.VENDOR) + .parse(String.format("%s %s", + requireIdentifierNonNull(identifier, ContactType.VENDOR, + UpdateCommand.MESSAGE_USAGE).substring(1), arguments)); + + case AddServiceCommand.COMMAND_WORD_VENDOR: + return new AddServiceCommandParser(ContactType.VENDOR) + .parse(String.format("%s %s", + requireIdentifierNonNull(identifier, ContactType.VENDOR, + AddServiceCommand.MESSAGE_USAGE).substring(1), arguments)); + default: throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } } + /** + * Ensure that the {@code String} object is non-null. + * @param identifier The {@code String} object that is to be non-null + * @param contactType The {@code ContactType} of the command. + * @param messageUsage The correct usage of the command. + * @return The {@code String} object, if it's non-null + * @throws ParseException If {@code String} object is actually null + */ + private String requireIdentifierNonNull(String identifier, ContactType contactType, String messageUsage) + throws ParseException { + if (identifier == null) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + String.format(messageUsage, contactType, "#"))); + } + + return identifier; + } + + /** + * Ensure that the {@code String} object is empty. + * @param args The {@code String} object that is to be empty. + * @param contactType The {@code ContactType} of the command. + * @param messageUsage The correct usage of the command. + * @throws ParseException If {@code String} object is not empty. + */ + private void requireEmptyArguments(String args, ContactType contactType, String messageUsage) + throws ParseException { + if (args.trim().length() > 0) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + String.format(messageUsage, contactType, "#"))); + } + } + + /** + * Ensure that the {@code String} object is null. This returns a more specific command format message. + * @param identifier The {@code String} object that is to be null. + * @param contactType The {@code ContactType} of the command. + * @param messageUsage The correct usage of the command. + * @throws ParseException If {@code String} object is not null + */ + private void requireNullIdentifier(String identifier, ContactType contactType, String messageUsage) + throws ParseException { + if (identifier != null) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + String.format(messageUsage, contactType, "#"))); + } + } + + /** + * Ensure that the {@code String} object is null. This returns a general invalid command format message. + * @param identifier The {@code String} object that is to be null. + * @throws ParseException If {@code String} object is not null + */ + private void requireNullIdentifier(String identifier, String messageUsage) throws ParseException { + if (identifier != null) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + messageUsage)); + } + } } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java index 954c8e18f8ea..17fa63d3a59d 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java @@ -57,4 +57,8 @@ public List getAllValues(Prefix prefix) { public String getPreamble() { return getValue(new Prefix("")).orElse(""); } + + public boolean isEmpty() { + return argMultimap.size() == 1 && getPreamble().equals(""); + } } diff --git a/src/main/java/seedu/address/logic/parser/AutoMatchCommandParser.java b/src/main/java/seedu/address/logic/parser/AutoMatchCommandParser.java new file mode 100644 index 000000000000..d2cda3eb51d7 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AutoMatchCommandParser.java @@ -0,0 +1,40 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_CONTACT_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; + +import seedu.address.logic.commands.AutoMatchCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code AutoMatchCommand} object + */ +public class AutoMatchCommandParser implements Parser { + + private static final int ENTITY_TYPE = 0; + private static final int ENTITY_ID = 1; + + /** + * Parses the given {@code String} of arguments in the context of the {@code AutoMatchCommand} and returns an + * {@code AutoMatchCommand} object for execution. + */ + public AutoMatchCommand parse(String args) throws ParseException, NumberFormatException { + + String[] entity = args.trim().split("#"); + String entityType = entity[ENTITY_TYPE]; + String entityId; + + try { + entityId = entity[ENTITY_ID]; + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParseException(String.format(MESSAGE_INVALID_CONTACT_FORMAT, args)); + } + + try { + return new AutoMatchCommand(entityType, Integer.parseInt(entityId)); + } catch (NumberFormatException exception) { + throw new ParseException(String.format(MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, entityId)); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf1190..091cadccc019 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -11,5 +11,7 @@ public class CliSyntax { public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_SERVICE = new Prefix("s/"); + public static final Prefix PREFIX_COST = new Prefix("c/"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 4d1f4bb0e4ec..2ea066564a48 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -1,16 +1,23 @@ package seedu.address.logic.parser; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.ContactType; /** * Parses input arguments and creates a new DeleteCommand object */ public class DeleteCommandParser implements Parser { + private final ContactType contactType; + + public DeleteCommandParser(ContactType contactType) { + this.contactType = contactType; + } + /** * Parses the given {@code String} of arguments in the context of the DeleteCommand * and returns an DeleteCommand object for execution. @@ -18,11 +25,10 @@ public class DeleteCommandParser implements Parser { */ 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); + Index id = ParserUtil.parseIndex(args.substring(1)); + return new DeleteCommand(id, contactType); + } catch (ParseException e) { + throw new ParseException(String.format(MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, args.substring(1))); } } diff --git a/src/main/java/seedu/address/logic/parser/EditPasswordCommandParser.java b/src/main/java/seedu/address/logic/parser/EditPasswordCommandParser.java new file mode 100644 index 000000000000..87caf2e346ba --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditPasswordCommandParser.java @@ -0,0 +1,67 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Optional; +import java.util.stream.Stream; + +import seedu.address.logic.commands.EditPasswordCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments for editing user password. + */ +public class EditPasswordCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the LoginCommand + * and returns a LoginCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public EditPasswordCommand parse(String userInput) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(userInput, + EditPasswordCommand.PREFIX_OLDPASSWORD, EditPasswordCommand.PREFIX_NEWPASSWORD); + + if (!arePrefixesPresent(argMultimap, EditPasswordCommand.PREFIX_OLDPASSWORD, + EditPasswordCommand.PREFIX_NEWPASSWORD) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditPasswordCommand.MESSAGE_USAGE)); + } + + Optional oldPassword = argMultimap.getValue(EditPasswordCommand.PREFIX_OLDPASSWORD); + Optional newPassword = argMultimap.getValue(EditPasswordCommand.PREFIX_NEWPASSWORD); + + if (oldPassword.isPresent() && newPassword.isPresent()) { + ensureFieldNotEmptyString(oldPassword, EditPasswordCommand.MESSAGE_FAILURE_EMPTYOLDPASSWORD); + ensureFieldNotEmptyString(newPassword, EditPasswordCommand.MESSAGE_FAILURE_EMPTYNEWPASSWORD); + ensureFieldDoesNotContainSpace(oldPassword, EditPasswordCommand.MESSAGE_FAILURE_OLDPASSWORDWITHSPACE); + ensureFieldDoesNotContainSpace(newPassword, EditPasswordCommand.MESSAGE_FAILURE_NEWPASSWORDWITHSPACE); + + return new EditPasswordCommand(oldPassword.get(), newPassword.get()); + } + + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditPasswordCommand.MESSAGE_USAGE)); + } + + private void ensureFieldNotEmptyString(Optional field, String commandFailureMessage) throws ParseException { + if (field.get().equals("")) { + throw new ParseException(commandFailureMessage); + } + } + + private void ensureFieldDoesNotContainSpace(Optional field, String commandFailureMessage) + throws ParseException { + if (field.get().contains(" ")) { + throw new ParseException(commandFailureMessage); + } + } + + /** + * 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/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java deleted file mode 100644 index b186a967cb94..000000000000 --- 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 an 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/ListCommandParser.java b/src/main/java/seedu/address/logic/parser/ListCommandParser.java new file mode 100644 index 000000000000..a4064f0950e3 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ListCommandParser.java @@ -0,0 +1,78 @@ +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.List; +import java.util.Optional; +import java.util.stream.Stream; + +import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.ContactType; +import seedu.address.model.contact.ContactContainsKeywordsPredicate; +import seedu.address.model.contact.ContactInformation; + +/** + * Parses input arguments and creates a new ListCommand object + */ +public class ListCommandParser implements Parser { + + private final ContactType contactType; + + public ListCommandParser(ContactType contactType) { + this.contactType = contactType; + } + + /** + * Parses the given {@code String} of arguments in the context of the ListCommand + * and returns an ListCommand object for execution. + */ + public ListCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = createLegalArgumentMultimap(args); + + Optional name = argMultimap.getValue(PREFIX_NAME); + Optional phone = argMultimap.getValue(PREFIX_PHONE); + Optional email = argMultimap.getValue(PREFIX_EMAIL); + Optional address = argMultimap.getValue(PREFIX_ADDRESS); + List tagList = argMultimap.getAllValues(PREFIX_TAG); + + return new ListCommand(new ContactContainsKeywordsPredicate( + new ContactInformation(name, phone, email, address, tagList)), contactType); + } + + /** + * Creates a {@code ArgumentMultimap} using the arguments from the input. + * @param args The arguments from the input + * @return the {@code ArgumentMultimap} generated using the input arguments. + * @throws ParseException If a legal {@code ArgumentMultimap} is not able to be created due to an invalid command + * format. + */ + protected ArgumentMultimap createLegalArgumentMultimap(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, PREFIX_TAG) + && !argMultimap.isEmpty()) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + String.format(ListCommand.MESSAGE_USAGE, contactType))); + } + + return argMultimap; + } + + + /** + * Returns true if at least one of the prefixes contains non-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).orElse("empty").equals("")); + } +} diff --git a/src/main/java/seedu/address/logic/parser/LoginCommandParser.java b/src/main/java/seedu/address/logic/parser/LoginCommandParser.java new file mode 100644 index 000000000000..3ca18b451401 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/LoginCommandParser.java @@ -0,0 +1,49 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Optional; +import java.util.stream.Stream; + +import seedu.address.logic.commands.LoginCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments + */ +public class LoginCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the LoginCommand + * and returns a LoginCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public LoginCommand parse(String userInput) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(userInput, + LoginCommand.PREFIX_USERNNAME, LoginCommand.PREFIX_PASSWORD); + + if (!arePrefixesPresent(argMultimap, LoginCommand.PREFIX_USERNNAME, + LoginCommand.PREFIX_PASSWORD) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, LoginCommand.MESSAGE_USAGE)); + } + + Optional username = argMultimap.getValue(LoginCommand.PREFIX_USERNNAME); + Optional password = argMultimap.getValue(LoginCommand.PREFIX_PASSWORD); + + if (username.isPresent() && password.isPresent()) { + return new LoginCommand(username.get(), password.get()); + } + + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, LoginCommand.MESSAGE_USAGE)); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index 76daf40807e2..0da0a61fcd72 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -9,10 +9,10 @@ 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.contact.Address; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Phone; import seedu.address.model.tag.Tag; /** diff --git a/src/main/java/seedu/address/logic/parser/RegisterAccountCommandParser.java b/src/main/java/seedu/address/logic/parser/RegisterAccountCommandParser.java new file mode 100644 index 000000000000..a98c6c36de05 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RegisterAccountCommandParser.java @@ -0,0 +1,81 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Optional; +import java.util.stream.Stream; + +import seedu.address.logic.commands.RegisterAccountCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.account.Account; +import seedu.address.model.account.Role; + +/** + * Parse input arguments. + */ +public class RegisterAccountCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the LoginCommand + * and returns a LoginCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public RegisterAccountCommand parse(String userInput) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(userInput, + RegisterAccountCommand.PREFIX_USERNNAME, RegisterAccountCommand.PREFIX_PASSWORD, + RegisterAccountCommand.PREFIX_ROLE); + + if (!arePrefixesPresent(argMultimap, RegisterAccountCommand.PREFIX_USERNNAME, + RegisterAccountCommand.PREFIX_PASSWORD, RegisterAccountCommand.PREFIX_ROLE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + RegisterAccountCommand.MESSAGE_USAGE)); + } + + Optional username = argMultimap.getValue(RegisterAccountCommand.PREFIX_USERNNAME); + Optional password = argMultimap.getValue(RegisterAccountCommand.PREFIX_PASSWORD); + Optional role = argMultimap.getValue(RegisterAccountCommand.PREFIX_ROLE); + + if (username.isPresent() && password.isPresent() && role.isPresent()) { + String roleName = role.get(); + + ensureFieldNotEmptyString(username, RegisterAccountCommand.MESSAGE_FAILURE_EMPTYUSERNAME); + ensureFieldNotEmptyString(password, RegisterAccountCommand.MESSAGE_FAILURE_EMPTYPASSWORD); + ensureFieldDoesNotContainSpace(username, RegisterAccountCommand.MESSAGE_FAILURE_USERNAMEWITHSPACE); + ensureFieldDoesNotContainSpace(password, RegisterAccountCommand.MESSAGE_FAILURE_PASSWORDWITHSPACE); + + if (roleName.equalsIgnoreCase("superuser")) { + Account account = new Account(username.get(), password.get(), Role.SUPER_USER); + return new RegisterAccountCommand(account); + } else if (roleName.equalsIgnoreCase("readonlyuser")) { + Account account = new Account(username.get(), password.get(), Role.READ_ONLY_USER); + return new RegisterAccountCommand(account); + } else { + throw new ParseException(RegisterAccountCommand.MESSAGE_INVALIDROLE); + } + } + + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, RegisterAccountCommand.MESSAGE_USAGE)); + } + + private void ensureFieldNotEmptyString(Optional field, String commandFailureMessage) throws ParseException { + if (field.get().equals("")) { + throw new ParseException(commandFailureMessage); + } + } + + private void ensureFieldDoesNotContainSpace(Optional field, String commandFailureMessage) + throws ParseException { + if (field.get().contains(" ")) { + throw new ParseException(commandFailureMessage); + } + } + + /** + * 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/SelectCommandParser.java b/src/main/java/seedu/address/logic/parser/SelectCommandParser.java index 565b7f04bfe1..57ae51f4bdde 100644 --- a/src/main/java/seedu/address/logic/parser/SelectCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/SelectCommandParser.java @@ -1,16 +1,23 @@ package seedu.address.logic.parser; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.SelectCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.ContactType; /** * Parses input arguments and creates a new SelectCommand object */ public class SelectCommandParser implements Parser { + private ContactType contactType; + + public SelectCommandParser(ContactType contactType) { + this.contactType = contactType; + } + /** * Parses the given {@code String} of arguments in the context of the SelectCommand * and returns an SelectCommand object for execution. @@ -18,11 +25,10 @@ public class SelectCommandParser implements Parser { */ public SelectCommand parse(String args) throws ParseException { try { - Index index = ParserUtil.parseIndex(args); - return new SelectCommand(index); - } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectCommand.MESSAGE_USAGE), pe); + Index id = ParserUtil.parseIndex(args.substring(1)); + return new SelectCommand(id, contactType); + } catch (ParseException e) { + throw new ParseException(String.format(MESSAGE_INVALID_PERSON_DISPLAYED_INDEX, args.substring(1))); } } } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/UpdateCommandParser.java similarity index 72% rename from src/main/java/seedu/address/logic/parser/EditCommandParser.java rename to src/main/java/seedu/address/logic/parser/UpdateCommandParser.java index 845644b7dea1..7729d1f96505 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/UpdateCommandParser.java @@ -1,6 +1,5 @@ package seedu.address.logic.parser; -import static java.util.Objects.requireNonNull; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; @@ -14,35 +13,41 @@ import java.util.Set; import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.UpdateCommand; +import seedu.address.logic.commands.UpdateCommand.EditContactDescriptor; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.ContactType; import seedu.address.model.tag.Tag; /** - * Parses input arguments and creates a new EditCommand object + * Parses input arguments and creates a new UpdateCommand object */ -public class EditCommandParser implements Parser { +public class UpdateCommandParser implements Parser { + private final ContactType contactType; + + public UpdateCommandParser(ContactType contactType) { + this.contactType = contactType; + } /** - * 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 UpdateCommand + * and returns an UpdateCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ - public EditCommand parse(String args) throws ParseException { - requireNonNull(args); + public UpdateCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - Index index; + Index id; try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); + id = 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(MESSAGE_INVALID_COMMAND_FORMAT, + String.format(UpdateCommand.MESSAGE_USAGE, contactType, "#"))); } - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + UpdateCommand.EditContactDescriptor editPersonDescriptor = new EditContactDescriptor(); if (argMultimap.getValue(PREFIX_NAME).isPresent()) { editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); } @@ -58,10 +63,10 @@ public EditCommand parse(String args) throws ParseException { parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + throw new ParseException(UpdateCommand.MESSAGE_NOT_EDITED); } - return new EditCommand(index, editPersonDescriptor); + return new UpdateCommand(id, editPersonDescriptor, contactType); } /** diff --git a/src/main/java/seedu/address/logic/security/PasswordAuthentication.java b/src/main/java/seedu/address/logic/security/PasswordAuthentication.java new file mode 100644 index 000000000000..1260ca8184a8 --- /dev/null +++ b/src/main/java/seedu/address/logic/security/PasswordAuthentication.java @@ -0,0 +1,137 @@ +package seedu.address.logic.security; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +/** + * Hash passwords for storage, and test passwords against password tokens. + *

+ * Instances of this class can be used concurrently by multiple threads. + * + * @author erickson + * @see StackOverflow + */ +public final class PasswordAuthentication { + + /** + * Each token produced by this class uses this identifier as a prefix. + */ + public static final String ID = "$31$"; + + /** + * The minimum recommended cost, used by default + */ + public static final int DEFAULT_COST = 16; + + private static final String ALGORITHM = "PBKDF2WithHmacSHA512"; + + private static final int SIZE = 128; + + private static final Pattern layout = Pattern.compile("\\$31\\$(\\d\\d?)\\$(.{43})"); + + private final SecureRandom random; + + private final int cost; + + public PasswordAuthentication() { + this(DEFAULT_COST); + } + + /** + * Create a password manager with a specified cost + * + * @param cost the exponential computational cost of hashing a password, 0 to 30 + */ + public PasswordAuthentication(int cost) { + int dummy = iterations(cost); /* Validate cost */ + this.cost = cost; + this.random = new SecureRandom(); + } + + public static String getHashedPasswordFromPlainText(String plainText) { + PasswordAuthentication passwordAuthentication = new PasswordAuthentication(); + return passwordAuthentication.hash(plainText.toCharArray()); + } + + /** + * Conduct an iteration. + */ + private static int iterations(int cost) { + if ((cost < 0) || (cost > 30)) { + throw new IllegalArgumentException("cost: " + cost); + } + return 1 << cost; + } + + /** + * Hash a password for storage. + * + * @return a secure authentication token to be stored for later authentication + */ + public String hash(char[] password) { + byte[] salt = new byte[SIZE / 8]; + random.nextBytes(salt); + byte[] dk = pbkdf2(password, salt, 1 << cost); + byte[] hash = new byte[salt.length + dk.length]; + System.arraycopy(salt, 0, hash, 0, salt.length); + System.arraycopy(dk, 0, hash, salt.length, dk.length); + Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding(); + return ID + cost + '$' + enc.encodeToString(hash); + } + + /** + * Hash a password in an immutable {@code String}. + *

+ *

Passwords should be stored in a {@code char[]} so that it can be filled + * with zeros after use instead of lingering on the heap and elsewhere. + * + */ + public String hash(String password) { + return hash(password.toCharArray()); + } + + /** + * Authenticate with a password and a stored password token. + * + * @return true if the password and token match + */ + public boolean authenticate(char[] password, String token) { + Matcher m = layout.matcher(token); + if (!m.matches()) { + throw new IllegalArgumentException("Invalid token format"); + } + int iterations = iterations(Integer.parseInt(m.group(1))); + byte[] hash = Base64.getUrlDecoder().decode(m.group(2)); + byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8); + byte[] check = pbkdf2(password, salt, iterations); + int zero = 0; + for (int idx = 0; idx < check.length; ++idx) { + zero |= hash[salt.length + idx] ^ check[idx]; + } + return zero == 0; + } + + /** + * Use the pbkdf2 algorithm provided by java + */ + private static byte[] pbkdf2(char[] password, byte[] salt, int iterations) { + KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE); + try { + SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM); + return f.generateSecret(spec).getEncoded(); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex); + } catch (InvalidKeySpecException ex) { + throw new IllegalStateException("Invalid SecretKeyFactory", ex); + } + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 7f85c8b9258b..3e052938cb14 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -5,16 +5,16 @@ import java.util.List; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.UniqueContactList; /** * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) + * Duplicates are not allowed (by .isSameContact comparison) */ public class AddressBook implements ReadOnlyAddressBook { - private final UniquePersonList persons; + private final UniqueContactList contacts; /* * The 'unusual' code block below is an non-static initialization block, sometimes used to avoid duplication @@ -24,13 +24,13 @@ public class AddressBook implements ReadOnlyAddressBook { * among constructors. */ { - persons = new UniquePersonList(); + contacts = new UniqueContactList<>(); } public AddressBook() {} /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} + * Creates an AddressBook using the Contacts in the {@code toBeCopied} */ public AddressBook(ReadOnlyAddressBook toBeCopied) { this(); @@ -40,11 +40,11 @@ public AddressBook(ReadOnlyAddressBook toBeCopied) { //// list overwrite operations /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. + * Replaces the contents of the contact list with {@code contacts}. + * {@code contacts} must not contain duplicate contacts. */ - public void setPersons(List persons) { - this.persons.setPersons(persons); + private void setContacts(List contacts) { + this.contacts.setContacts(contacts); } /** @@ -53,68 +53,69 @@ public void setPersons(List persons) { public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); - setPersons(newData.getPersonList()); + setContacts(newData.getContactList()); } - //// person-level operations + //// contact-level operations /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a contact with the same identity as {@code contact} exists in the address book. */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); + public boolean hasContact(Contact contact) { + requireNonNull(contact); + return contacts.contains(contact); } /** - * Adds a person to the address book. - * The person must not already exist in the address book. + * Adds a contact to the address book. + * The contact must not already exist in the address book. */ - public void addPerson(Person p) { - persons.add(p); + public void addContact(Contact p) { + contacts.add(p); } /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. + * Replaces the given contact {@code target} in the list with {@code editedContact}. * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * The contact identity of {@code editedContact} must not be the same as another existing contact in the address + * book. */ - public void updatePerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); + public void updateContact(Contact target, Contact editedContact) { + requireNonNull(editedContact); - persons.setPerson(target, editedPerson); + contacts.setContact(target, editedContact); } /** * Removes {@code key} from this {@code AddressBook}. * {@code key} must exist in the address book. */ - public void removePerson(Person key) { - persons.remove(key); + public void removeContact(Contact key) { + contacts.remove(key); } //// util methods @Override public String toString() { - return persons.asUnmodifiableObservableList().size() + " persons"; + return contacts.asUnmodifiableObservableList().size() + " contacts"; // TODO: refine later } @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); + public ObservableList getContactList() { + return contacts.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)); + && contacts.equals(((AddressBook) other).contacts)); } @Override public int hashCode() { - return persons.hashCode(); + return contacts.hashCode(); } } diff --git a/src/main/java/seedu/address/model/AutoMatchResult.java b/src/main/java/seedu/address/model/AutoMatchResult.java new file mode 100644 index 000000000000..e8f66cb9bf2b --- /dev/null +++ b/src/main/java/seedu/address/model/AutoMatchResult.java @@ -0,0 +1,82 @@ +package seedu.address.model; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Service; + +/** + * The results from an auto-matching operation. + */ +public class AutoMatchResult { + private final Map> resultMap; + private final Contact target; + + public AutoMatchResult(Contact target) { + this(target, new HashMap<>()); + } + + private AutoMatchResult(Contact target, Map> resultMap) { + this.target = target; + this.resultMap = resultMap; + } + + /** + * Adds a {@code Collection} of {@code Service} that the {@code Contact} was shortlisted for. + * @param contact + * @param services + */ + public void put(Contact contact, Collection services) { + if (services.size() > 0) { + resultMap.put(contact, services); + } + } + + /** + * Gets a {@code Collection} of {@code Contact} that was shortlisted. + * @return + */ + public Collection getContacts() { + return resultMap.keySet(); + } + + /** + * Gets the mapping of a {@code Collection} of {@code Service} that was matched for the {@code Contact}. + * @return + */ + public Map> getContactAndServicesMap() { + return resultMap; + } + + /** + * Prunes all {@code Contact} from {@code resultMap} that does not have at least one {@code Service}. + */ + private void prune() { + Collection contactsToPruneStream = resultMap + .keySet() + .stream() + .filter(contact -> resultMap.get(contact).size() < 1) + .collect(Collectors.toList()); + for (Contact contact : contactsToPruneStream) { + resultMap.remove(contact); + } + } + + /** + * Merges 2 {@code AutoMatchResult} into one. To be used as a combiner. + * @param resultA First result. + * @param resultB Second result. + * @return Merged result. + */ + public static AutoMatchResult mergeResults(AutoMatchResult resultA, AutoMatchResult resultB) { + Map> resultMap = Stream.of(resultA.resultMap, resultB.resultMap) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + AutoMatchResult newResult = new AutoMatchResult(resultA.target, resultMap); + return newResult; + } +} diff --git a/src/main/java/seedu/address/model/ContactType.java b/src/main/java/seedu/address/model/ContactType.java new file mode 100644 index 000000000000..4d720d7fc676 --- /dev/null +++ b/src/main/java/seedu/address/model/ContactType.java @@ -0,0 +1,61 @@ +package seedu.address.model; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_CONTACT_TYPE; + +import java.util.function.Predicate; + +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.client.Client; +import seedu.address.model.contact.Contact; +import seedu.address.model.vendor.Vendor; + +/** + * Specifies the different contact types. This enum includes the correct filter required to filter a specific + * contact type from a list of contacts of different contact types. + */ +public enum ContactType { + CLIENT { + @Override + public Predicate getFilter() { + return contact -> contact instanceof Client; + } + + @Override + public String toString() { + return CLIENT_STRING; + } + }, + VENDOR { + @Override + public Predicate getFilter() { + return contact -> contact instanceof Vendor; + } + + @Override + public String toString() { + return VENDOR_STRING; + } + }; + + private static final String CLIENT_STRING = "client"; + private static final String VENDOR_STRING = "vendor"; + + public abstract Predicate getFilter(); + + /** + * Utility static function to get a {@code ContactType} from a {@code string}. + * + * @param string The string to convert from. + * @return The contact type from the string. + */ + public static ContactType fromString(String string) throws ParseException { + switch (string) { + case CLIENT_STRING: + return CLIENT; + case VENDOR_STRING: + return VENDOR; + default: + throw new ParseException(String.format(MESSAGE_INVALID_CONTACT_TYPE, string)); + } + } +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index ac4521f33199..ff23cba0b80a 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -3,14 +3,16 @@ import java.util.function.Predicate; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; +import seedu.address.model.account.Account; +import seedu.address.model.contact.Contact; /** * The API of the Model component. */ public interface Model { /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + //TODO: this predicate shows contacts only. javadocs is incorrect here but KIV first + Predicate PREDICATE_SHOW_ALL_PERSONS = contact -> true; /** Clears existing backing model and replaces with the provided new data. */ void resetData(ReadOnlyAddressBook newData); @@ -19,37 +21,38 @@ public interface Model { ReadOnlyAddressBook getAddressBook(); /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a contact with the same identity as {@code contact} exists in the address book. */ - boolean hasPerson(Person person); + boolean hasContact(Contact contact); /** - * Deletes the given person. - * The person must exist in the address book. + * Deletes the given contact. + * The contact must exist in the address book. */ - void deletePerson(Person target); + void deleteContact(Contact target); /** - * Adds the given person. - * {@code person} must not already exist in the address book. + * Adds the given contact. + * {@code contact} must not already exist in the address book. */ - void addPerson(Person person); + void addContact(Contact contact); /** - * Replaces the given person {@code target} with {@code editedPerson}. + * Replaces the given contact {@code target} with {@code editedContact}. * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * The contact identity of {@code editedContact} must not be the same as another existing contact in the address + * book. */ - void updatePerson(Person target, Person editedPerson); + void updateContact(Contact target, Contact editedContact); - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered contact list */ + ObservableList getFilteredContactList(); /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. + * Updates the filter of the filtered contact list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ - void updateFilteredPersonList(Predicate predicate); + void updateFilteredContactList(Predicate predicate); /** * Returns true if the model has previous address book states to restore. @@ -75,4 +78,44 @@ public interface Model { * Saves the current address book state for undo/redo. */ void commitAddressBook(); + + /** + * The user has logged in with an account successfully. Saves this account + * into + * @param account The account user used to log in successfully + */ + void commitUserLoggedInSuccessfully(Account account); + + /** + * Get the user account which he used to logged in to this application. + * @return The account used to logged in to this application. + */ + Account getUserAccount(); + + /** + * Return true if user has logged in successfully, false otherwise. + * @return true if user has logged in successfully, false otherwise. + */ + boolean isUserLoggedIn(); + + /** + * The user has logged out of his account successfully. + */ + void commitUserLoggedOutSuccessfully(); + + /** + * The user has changed his old password to {@code newPassword}. + */ + void commiteUserChangedPasswordSuccessfully(String newPassword); + + /** + * Updates the auto-matching results. + * @param newResults The new results to replace the old one. + */ + void updateAutoMatchResult(AutoMatchResult newResults); + + /** + * Retrieves the last updated results for the auto-matching. + */ + AutoMatchResult getAutoMatchResult(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index a664602ef5b1..f9475941c5c9 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -12,7 +12,8 @@ import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.model.person.Person; +import seedu.address.model.account.Account; +import seedu.address.model.contact.Contact; /** * Represents the in-memory model of the address book data. @@ -21,7 +22,9 @@ public class ModelManager extends ComponentManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); private final VersionedAddressBook versionedAddressBook; - private final FilteredList filteredPersons; + private final FilteredList filteredContacts; + private Account userAccount; + private AutoMatchResult autoMatchResult = null; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -30,16 +33,28 @@ public ModelManager(ReadOnlyAddressBook addressBook, UserPrefs userPrefs) { super(); requireAllNonNull(addressBook, userPrefs); - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); + logger.fine("Initializing with Heart²: " + addressBook + " and user prefs " + userPrefs); versionedAddressBook = new VersionedAddressBook(addressBook); - filteredPersons = new FilteredList<>(versionedAddressBook.getPersonList()); + filteredContacts = new FilteredList<>(versionedAddressBook.getContactList()); + // initial: agreed to show client list + updateFilteredContactList(ContactType.CLIENT.getFilter()); } public ModelManager() { this(new AddressBook(), new UserPrefs()); } + public ModelManager(Account userAccount) { + this(new AddressBook(), new UserPrefs()); + this.userAccount = userAccount; + } + + public ModelManager(ReadOnlyAddressBook addressBook, UserPrefs userPrefs, Account userAccount) { + this(addressBook, userPrefs); + this.userAccount = userAccount; + } + @Override public void resetData(ReadOnlyAddressBook newData) { versionedAddressBook.resetData(newData); @@ -57,47 +72,58 @@ private void indicateAddressBookChanged() { } @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return versionedAddressBook.hasPerson(person); + public boolean hasContact(Contact contact) { + requireNonNull(contact); + return versionedAddressBook.hasContact(contact); } @Override - public void deletePerson(Person target) { - versionedAddressBook.removePerson(target); + public void deleteContact(Contact target) { + versionedAddressBook.removeContact(target); indicateAddressBookChanged(); } @Override - public void addPerson(Person person) { - versionedAddressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + public void addContact(Contact contact) { + versionedAddressBook.addContact(contact); + updateFilteredContactList(contact.getType().getFilter()); indicateAddressBookChanged(); } @Override - public void updatePerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); + public void updateContact(Contact target, Contact editedContact) { + requireAllNonNull(target, editedContact); - versionedAddressBook.updatePerson(target, editedPerson); + versionedAddressBook.updateContact(target, editedContact); indicateAddressBookChanged(); } - //=========== Filtered Person List Accessors ============================================================= + //=========== Filtered Client List Accessors ============================================================= /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of + * Returns an unmodifiable view of the list of {@code Client} backed by the internal list of * {@code versionedAddressBook} */ @Override - public ObservableList getFilteredPersonList() { - return FXCollections.unmodifiableObservableList(filteredPersons); + public ObservableList getFilteredContactList() { + return FXCollections.unmodifiableObservableList(filteredContacts); } @Override - public void updateFilteredPersonList(Predicate predicate) { + public void updateFilteredContactList(Predicate predicate) { requireNonNull(predicate); - filteredPersons.setPredicate(predicate); + filteredContacts.setPredicate(predicate); + } + + //=========== Auto-matching Accessors =================================================================== + + @Override + public void updateAutoMatchResult(AutoMatchResult newResults) { + autoMatchResult = newResults; + } + + public AutoMatchResult getAutoMatchResult() { + return autoMatchResult; } //=========== Undo/Redo ================================================================================= @@ -129,6 +155,33 @@ public void commitAddressBook() { versionedAddressBook.commit(); } + @Override + public void commitUserLoggedInSuccessfully(Account userAccount) { + this.userAccount = userAccount; + } + + @Override + public Account getUserAccount() { + requireNonNull(userAccount); + return userAccount; + } + + @Override + public boolean isUserLoggedIn() { + return userAccount != null; + } + + @Override + public void commitUserLoggedOutSuccessfully() { + userAccount = null; + versionedAddressBook.clearState(); + } + + @Override + public void commiteUserChangedPasswordSuccessfully(String newPassword) { + userAccount = new Account(userAccount.getUserName(), newPassword, userAccount.getRole()); + } + @Override public boolean equals(Object obj) { // short circuit if same object @@ -144,7 +197,7 @@ public boolean equals(Object obj) { // state check ModelManager other = (ModelManager) obj; return versionedAddressBook.equals(other.versionedAddressBook) - && filteredPersons.equals(other.filteredPersons); + && filteredContacts.equals(other.filteredContacts); } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a290..ad95fd0eceea 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,7 +1,7 @@ package seedu.address.model; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** * Unmodifiable view of an address book @@ -12,6 +12,6 @@ public interface ReadOnlyAddressBook { * Returns an unmodifiable view of the persons list. * This list will not contain any duplicate persons. */ - ObservableList getPersonList(); + ObservableList getContactList(); } diff --git a/src/main/java/seedu/address/model/Searchable.java b/src/main/java/seedu/address/model/Searchable.java new file mode 100644 index 000000000000..3fd60bf39c57 --- /dev/null +++ b/src/main/java/seedu/address/model/Searchable.java @@ -0,0 +1,18 @@ +package seedu.address.model; + +import java.util.List; +import java.util.stream.Stream; + +/** + * Searchable model + */ +public interface Searchable { + /** + * Filters a stream of Searchable for the specified query string. + * + * @param searchableStream Source stream + * @param queryString Query string + * @return + */ + List search(Stream searchableStream, String queryString); +} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 980b2b388852..167837bd085c 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -11,8 +11,9 @@ */ public class UserPrefs { + private static String usernameAndRoleToDisplay = ""; private GuiSettings guiSettings; - private Path addressBookFilePath = Paths.get("data" , "addressbook.xml"); + private Path addressBookFilePath = Paths.get("data" , "heart2.xml"); public UserPrefs() { setGuiSettings(500, 500, 0, 0); @@ -38,6 +39,14 @@ public void setAddressBookFilePath(Path addressBookFilePath) { this.addressBookFilePath = addressBookFilePath; } + public static void setUsernameAndRoleToDisplay(String str) { + usernameAndRoleToDisplay = str; + } + + public static String getUsernameAndRoleToDisplay() { + return usernameAndRoleToDisplay; + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/VersionedAddressBook.java b/src/main/java/seedu/address/model/VersionedAddressBook.java index 227a335045d7..2d825fecb0f0 100644 --- a/src/main/java/seedu/address/model/VersionedAddressBook.java +++ b/src/main/java/seedu/address/model/VersionedAddressBook.java @@ -19,6 +19,16 @@ public VersionedAddressBook(ReadOnlyAddressBook initialState) { currentStatePointer = 0; } + /** + * Remove all states in the addressBookStateList except the latest one + */ + public void clearState() { + ReadOnlyAddressBook temp = addressBookStateList.get(addressBookStateList.size() - 1); + addressBookStateList.clear(); + addressBookStateList.add(temp); + currentStatePointer = 0; + } + /** * Saves a copy of the current {@code AddressBook} state at the end of the state list. * Undone states are removed from the state list. diff --git a/src/main/java/seedu/address/model/account/Account.java b/src/main/java/seedu/address/model/account/Account.java new file mode 100644 index 000000000000..9e15670849e9 --- /dev/null +++ b/src/main/java/seedu/address/model/account/Account.java @@ -0,0 +1,106 @@ +package seedu.address.model.account; + +import seedu.address.logic.security.PasswordAuthentication; + +/** + * Account class represents a single Account that comprises of a username, + * password, and role associated with an account. + */ +public class Account { + public static final String USERNAME_CONSTRAINT = "Username cannot be empty."; + public static final String PASSWORD_CONSTRAINT = "Password cannot be empty."; + public static final String ROLE_CONSTRAINT = "Role is not specified."; + + private String username; + private String password; + private Role role; + + public Account(String username, String password) { + this.username = username; + this.password = password; + this.role = Role.READ_ONLY_USER; + } + + public Account(String username, String password, Role role) { + this.username = username; + this.password = password; + this.role = role; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getUserName() { + return username; + } + + public String getPassword() { + return password; + } + + public Role getRole() { + return role; + } + + public void transformToHashedAccount() { + this.password = PasswordAuthentication.getHashedPasswordFromPlainText(password); + } + + /** + * Write privilege refers to the ability to add or update contact. + * A user has write privilege if he is allowed to add and update existing contact. + * @return true if a user has write privilege, false otherwise. + */ + public boolean hasWritePrivilege() { + return role == Role.SUPER_USER; + } + + /** + * Delete privilege refers to the ability to delete a single contact, or all contact + * from the stored data. A user has delete privilege if he is allowed to delete + * existing stored data. + * @return true if a user has delete privilege, false otherwise. + */ + public boolean hasDeletePrivilege() { + return role == Role.SUPER_USER; + } + + /** + * Account creation privilege refers to the ability to create a new account with a + * username, password, and specifying a role. A user has account creation privilege + * if he is allowed to create a new account, either for himself, or for other people. + * @return true if a user has account creation privilege, false otherwise. + */ + public boolean hasAccountCreationPrivilege() { + return role == Role.SUPER_USER; + } + + /** + * The root user account hardcoded. + */ + public static Account getRootAccount() { + return new Account("rootUser", "rootPassword", Role.SUPER_USER); + } + + /** + * An Account is equal to another Account if both of them have the + * same username and password. + * @param other Other Account to compare + * @return true if both Accounts are equal, false otherwise. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Account)) { + return false; + } + + Account otherAccount = (Account) other; + return otherAccount.getUserName().equalsIgnoreCase(this.username) + && otherAccount.getPassword().equals(this.password); + } +} diff --git a/src/main/java/seedu/address/model/account/AccountList.java b/src/main/java/seedu/address/model/account/AccountList.java new file mode 100644 index 000000000000..7f6c5f652af0 --- /dev/null +++ b/src/main/java/seedu/address/model/account/AccountList.java @@ -0,0 +1,128 @@ +package seedu.address.model.account; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.logic.security.PasswordAuthentication; + +/** + * AccountList represents the entire list of accounts that were created + * and stored in the local database. Currently, all username and password are + * stored locally in a xml file without encryption. + */ +public class AccountList { + private List accountList; + + public AccountList() { + accountList = new ArrayList<>(); + } + + public List getList() { + return accountList; + } + + public void addAccount(Account account) { + accountList.add(account); + } + + /** + * Read from the lists of account and compare the provided account's username with an account stored in the + * account list that has the same username. If a match is found, return the Role of the account as specified + * in the account list. + * @param username The account to get the privilege. + * @return The role of the username specified in the account list file. + * @throws IllegalArgumentException If the provided username is not found in the account list. + */ + public Role getAccountRole(String username) throws IllegalArgumentException { + for (Account account : accountList) { + if (account.getUserName().equalsIgnoreCase(username)) { + return account.getRole(); + } + } + throw new IllegalArgumentException("Account not found."); + } + + /** + * Update the user associated with the {@code currentAccount} to the given password. + * @param username The username to update the account. + * @param plainTextPassword The password to update the account + */ + public void updatePassword(String username, String plainTextPassword) { + for (Account account : accountList) { + if (account.getUserName().equalsIgnoreCase(username)) { + account.setPassword(plainTextPassword); + } + } + } + + /** + * Returns the index of the first occurrence of the given account in this accountList, + * or -1 if accountList does not contain the account. + * @param account The account to be checked. + * @return The index of the first occurrence of the account in the list, or -1 if + * accountList does not contain the account. + */ + public int indexOfAccount(Account account) { + requireNonNull(account); + return accountList.indexOf(account); + } + + /** + * Returns true if accountList contains the given account. + * @param account The account to be checked. + * @return true if the accountList contains the account, false otherwise. + */ + public boolean hasAccount(Account account) { + requireNonNull(account); + return indexOfAccount(account) != -1; + } + + /** + * Return true if the account list contains the username, and the password provided + * is also correct. + * @param username The username of the account + * @param plainTextPassword The plaintext password of the account + * @return True if the username and password matches an existing account in the account list. + */ + public boolean hasUsernameAndPassword(String username, String plainTextPassword) { + PasswordAuthentication passwordAuthentication = new PasswordAuthentication(); + + for (Account account : accountList) { + if (account.getUserName().equalsIgnoreCase(username) + && passwordAuthentication.authenticate(plainTextPassword.toCharArray(), account.getPassword())) { + return true; + } + } + return false; + } + + /** + * Check if the accountList contains the username. + * @param username The username to be checked. + * @return true if accountList contains the username, false otherwise. + */ + public boolean hasUserName(String username) { + requireNonNull(username); + for (Account account : accountList) { + if (account.getUserName().equalsIgnoreCase(username)) { + return true; + } + } + + return false; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AccountList // instanceof handles nulls + && accountList.equals(((AccountList) other).accountList)); + } + + @Override + public int hashCode() { + return accountList.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/account/Role.java b/src/main/java/seedu/address/model/account/Role.java new file mode 100644 index 000000000000..cd1dc2fee54e --- /dev/null +++ b/src/main/java/seedu/address/model/account/Role.java @@ -0,0 +1,20 @@ +package seedu.address.model.account; + +/** + * A role refers to what privilege a user account has. A SUPER_USER is the user + * with full capabilities. A READ_ONLY_USER cannot add, edit, and delete any entries + * in the database. + */ +public enum Role { + SUPER_USER("super_user"), READ_ONLY_USER("read_only_user"); + + private String role; + + Role(String role) { + this.role = role; + } + + public String getRole() { + return this.role; + } +} diff --git a/src/main/java/seedu/address/model/client/Client.java b/src/main/java/seedu/address/model/client/Client.java new file mode 100644 index 000000000000..fba7c730e200 --- /dev/null +++ b/src/main/java/seedu/address/model/client/Client.java @@ -0,0 +1,79 @@ +package seedu.address.model.client; + +import java.util.Map; +import java.util.Set; + +import seedu.address.model.ContactType; +import seedu.address.model.contact.Address; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Service; +import seedu.address.model.tag.Tag; + +/** + * Represents a client in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Client extends Contact { + private static int clientId = 1; + + private final int id; + + /** + * Every field must be present and not null. + */ + public Client(Name name, Phone phone, Email email, Address address, Set tags) { + super(name, phone, email, address, tags); + this.id = clientId++; + } + + public Client(Name name, Phone phone, Email email, Address address, Set tags, Map services) { + super(name, phone, email, address, tags, services); + this.id = clientId++; + } + + public Client(Name name, Phone phone, Email email, + Address address, Set tags, Map services, int id) { + super(name, phone, email, address, tags, services); + this.id = id; + } + + public static void resetClientId() { + clientId = 1; + } + + @Override + public int getId() { + return id; + } + + @Override + public ContactType getType() { + return ContactType.CLIENT; + } + + /** + * Returns true if both clients have the same identity and data fields. + * This defines a stronger notion of equality between two clients. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Client)) { + return false; + } + + Client otherContact = (Client) other; + return otherContact.getName().equals(getName()) + && otherContact.getPhone().equals(getPhone()) + && otherContact.getEmail().equals(getEmail()) + && otherContact.getAddress().equals(getAddress()) + && otherContact.getTags().equals(getTags()) + && otherContact.getServices().equals(getServices()); + } +} diff --git a/src/main/java/seedu/address/model/client/exceptions/ClientNotFoundException.java b/src/main/java/seedu/address/model/client/exceptions/ClientNotFoundException.java new file mode 100644 index 000000000000..84f9ae2c085e --- /dev/null +++ b/src/main/java/seedu/address/model/client/exceptions/ClientNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.client.exceptions; + +/** + * Signals that the operation is unable to find the specified client. + */ +public class ClientNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/client/exceptions/DuplicateClientException.java similarity index 58% rename from src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java rename to src/main/java/seedu/address/model/client/exceptions/DuplicateClientException.java index d7290f594423..40a371862a15 100644 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ b/src/main/java/seedu/address/model/client/exceptions/DuplicateClientException.java @@ -1,11 +1,11 @@ -package seedu.address.model.person.exceptions; +package seedu.address.model.client.exceptions; /** * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same * identity). */ -public class DuplicatePersonException extends RuntimeException { - public DuplicatePersonException() { +public class DuplicateClientException extends RuntimeException { + public DuplicateClientException() { super("Operation would result in duplicate persons"); } } diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/contact/Address.java similarity index 94% rename from src/main/java/seedu/address/model/person/Address.java rename to src/main/java/seedu/address/model/contact/Address.java index a1409233ceb9..0123fa59fcc1 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/contact/Address.java @@ -1,10 +1,10 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's address in the address book. + * Represents a Client's address in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} */ public class Address { diff --git a/src/main/java/seedu/address/model/contact/Contact.java b/src/main/java/seedu/address/model/contact/Contact.java new file mode 100644 index 000000000000..2080a38bad03 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/Contact.java @@ -0,0 +1,148 @@ +package seedu.address.model.contact; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +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.stream.Collectors; +import java.util.stream.Stream; + +import seedu.address.model.ContactType; +import seedu.address.model.tag.Tag; + +/** + * An abstract class the client and the service provider inherits. + */ +public abstract class Contact { + // Identity fields + protected final Name name; + protected final Phone phone; + protected final Email email; + + // Data fields + protected final Address address; + protected final Set tags = new HashSet<>(); + protected final Map services = new HashMap<>(); + + public Contact(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 Contact(Name name, Phone phone, Email email, Address address, Set tags, Map services) { + requireAllNonNull(name, phone, email, address, tags, services); + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.tags.addAll(tags); + this.services.putAll(services); + } + + public abstract int getId(); + + public boolean hasService(Service service) { + return services.containsKey(service.getName()); + } + + // Get the name of the contact + public Name getName() { + return name; + } + + // Get the address of the contact + public Address getAddress() { + return address; + } + + // Get the email of the contact + public Email getEmail() { + return email; + } + + // get the phone number of the contact + public Phone getPhone() { + return phone; + } + + /** + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + // Get the contact type of this contact + public abstract ContactType getType(); + + // Get the services of the contact. + public Map getServices() { + return Collections.unmodifiableMap(services); + } + + // Get the services of the contact in a stream. + public Stream getServicesStream() { + return services.values().stream(); + } + + /** + * Returns true if both persons of the same name have at least one other identity field that is the same. + * This defines a weaker notion of equality between two persons. + */ + public boolean isSameContact(Contact otherContact) { + if (otherContact == this) { + return true; + } + + return otherContact != null + && otherContact.getName().equals(getName()) + && ((otherContact.getPhone().equals(getPhone()) || otherContact.getEmail().equals(getEmail()))) + && (otherContact.getClass().equals(this.getClass())); + } + + /** + * Concatenates the contact data into a URL String. + * @return URL of contact data. + */ + public String getUrlContactData() { + return "type=" + getType() + "&" + + "id=" + getId() + "&" + + "name=" + getName() + "&" + + "phone=" + getPhone() + "&" + + "email=" + getEmail() + "&" + + "address=" + getAddress() + "&" + + "tags=" + String.join(",", getTags().toString()) + "&" + + "services=" + String.join(",", + getServicesStream().map(Service::getUrlDescription).collect(Collectors.toList())); + } + + @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()) + .append(" Tags: "); + getTags().forEach(builder::append); + return builder.toString(); + } +} diff --git a/src/main/java/seedu/address/model/contact/ContactContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/contact/ContactContainsKeywordsPredicate.java new file mode 100644 index 000000000000..c4b5e2af5179 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/ContactContainsKeywordsPredicate.java @@ -0,0 +1,50 @@ +package seedu.address.model.contact; + +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Contact}'s properties matches with the keywords given. + */ +public class ContactContainsKeywordsPredicate implements Predicate { + private final ContactInformation keywords; + + public ContactContainsKeywordsPredicate() { + this.keywords = new ContactInformation(); + } + + public ContactContainsKeywordsPredicate(ContactInformation keywords) { + this.keywords = keywords; + } + + public ContactContainsKeywordsPredicate(Contact contact) { + this.keywords = new ContactInformation(contact); + } + + @Override + public boolean test(Contact contact) { + if (keywords.isEmpty()) { + return true; + } + + return keywords.getName().map(name -> StringUtil.containsIgnoreCase(contact.getName().toString(), name)) + .orElse(true) + && keywords.getPhone().map(phone -> StringUtil.containsIgnoreCase(contact.getPhone().toString(), phone)) + .orElse(true) + && keywords.getEmail().map(email -> StringUtil.containsIgnoreCase(contact.getEmail().toString(), email)) + .orElse(true) + && keywords.getAddress().map(address -> StringUtil.containsIgnoreCase(contact.getAddress().toString(), + address)).orElse(true) + && (keywords.getTags().stream().allMatch(tag -> StringUtil.containsIgnoreCase( + contact.getTags().toString(), tag)) + || keywords.getTags().isEmpty()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ContactContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((ContactContainsKeywordsPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/contact/ContactInformation.java b/src/main/java/seedu/address/model/contact/ContactInformation.java new file mode 100644 index 000000000000..3af174671cfd --- /dev/null +++ b/src/main/java/seedu/address/model/contact/ContactInformation.java @@ -0,0 +1,77 @@ +package seedu.address.model.contact; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Encapsulates information about a {@code Contact}, some of which may not be available. + */ +public class ContactInformation { + private Optional name; + private Optional phone; + private Optional email; + private Optional address; + private List tags; + + public ContactInformation () { + name = Optional.empty(); + phone = Optional.empty(); + email = Optional.empty(); + address = Optional.empty(); + tags = new ArrayList<>(); + } + + public ContactInformation (Optional name, Optional phone, Optional email, + Optional address, List tags) { + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.tags = tags; + } + + public ContactInformation (Contact contact) { + name = Optional.of(contact.getName().toString()); + phone = Optional.of(contact.getPhone().toString()); + email = Optional.of(contact.getEmail().toString()); + address = Optional.of(contact.getAddress().toString()); + tags = contact.getTags().stream().map(tag -> tag.toString()).collect(Collectors.toList()); + } + + public Optional getName() { + return name; + } + + public Optional getPhone() { + return phone; + } + + public Optional getEmail() { + return email; + } + + public Optional getAddress() { + return address; + } + + public List getTags() { + return tags; + } + + public boolean isEmpty() { + return !name.isPresent() && !phone.isPresent() && !email.isPresent() && !address.isPresent() && tags.isEmpty(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ContactInformation // instanceof handles nulls + && name.equals(((ContactInformation) other).name) + && phone.equals(((ContactInformation) other).phone) + && email.equals(((ContactInformation) other).email) + && address.equals(((ContactInformation) other).address) + && tags.equals(((ContactInformation) other).tags)); // state check + } +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/contact/Email.java similarity index 96% rename from src/main/java/seedu/address/model/person/Email.java rename to src/main/java/seedu/address/model/contact/Email.java index 38a7629e9a2d..55adc7480de1 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/contact/Email.java @@ -1,10 +1,10 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's email in the address book. + * Represents a Client's email in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} */ public class Email { diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/contact/Name.java similarity index 94% rename from src/main/java/seedu/address/model/person/Name.java rename to src/main/java/seedu/address/model/contact/Name.java index 9982393dabb5..93dd10e70110 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/contact/Name.java @@ -1,10 +1,10 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's name in the address book. + * Represents a Client's name in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ public class Name { diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/contact/Phone.java similarity index 93% rename from src/main/java/seedu/address/model/person/Phone.java rename to src/main/java/seedu/address/model/contact/Phone.java index a22e51653835..55cdf330b623 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/contact/Phone.java @@ -1,10 +1,10 @@ -package seedu.address.model.person; +package seedu.address.model.contact; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; /** - * Represents a Person's phone number in the address book. + * Represents a Client's phone number in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} */ public class Phone { diff --git a/src/main/java/seedu/address/model/contact/Service.java b/src/main/java/seedu/address/model/contact/Service.java new file mode 100644 index 000000000000..ae7f4187af82 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/Service.java @@ -0,0 +1,127 @@ +package seedu.address.model.contact; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Defines the service types + */ +enum ServiceType { + photographer, hotel, catering, dress, ring, transport, invitation +} + +/** + * Represents a Contact's Service in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidServiceName(String)} + */ +public class Service { + + public static final String MESSAGE_SERVICE_NAME_CONSTRAINTS = + "Valid Services: photographer, hotel, catering, dress, ring, transport, invitation (case insensitive)"; + public static final String MESSAGE_SERVICE_COST_CONSTRAINTS = + "Service cost must be more than $0.00 and given to 2 decimal places\n" + + "Please also omit all symbols except the decimal point"; + + private static final String COST_REGEX = "(\\d*)\\.(\\d{2})"; + private static final String NON_ZERO_REGEX = "[1-9]"; + + public final String serviceName; + public final BigDecimal serviceCost; + + // Id list of clients / service providers for service providers / clients respectively. + private List idList; + + /** + * Constructs a {@code Service}. + * + * @param service A valid service name. + * @param cost Cost of this service. + */ + public Service(String service, String cost) { + requireNonNull(service); + + checkArgument(isValidServiceName(service.toLowerCase()), MESSAGE_SERVICE_NAME_CONSTRAINTS); + checkArgument(isValidServiceCost(cost), MESSAGE_SERVICE_COST_CONSTRAINTS); + + serviceName = service.toLowerCase(); + serviceCost = new BigDecimal(cost);; + // Set to 2 decimal places + serviceCost.setScale(2); + idList = new ArrayList<>(); + } + + public String getName() { + return serviceName; + } + + public BigDecimal getCost() { + return serviceCost; + } + + /** + * Returns the URL description for the service. + * @return string describing service in URL format. + */ + public String getUrlDescription() { + return serviceName + ":" + serviceCost.toPlainString(); + } + + public List getIdList() { + return idList; + } + /** + * Returns true if a given string is a valid service name. + */ + public static boolean isValidServiceName(String test) { + for (ServiceType s : ServiceType.values()) { + if (s.name().equals(test.toLowerCase())) { + return true; + } + } + return false; + } + + /** + * Returns true if a given cost is above $0, has 2 decimal places and has no other symbols + */ + public static boolean isValidServiceCost(String test) { + Pattern pattern = Pattern.compile(NON_ZERO_REGEX); + Matcher matcher = pattern.matcher(test); + return test.matches(COST_REGEX) && matcher.find(); + } + + /** + * Returns true if the service type is the same as the {@code other} service specified. + * @param other The Service type. + * @return True if is the same type. + */ + public boolean isSameServiceTypeAs(Service other) { + return other != null + && serviceName.equals(other.serviceName); + } + + @Override + public String toString() { + return serviceName + " $" + serviceCost.toPlainString(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Service // instanceof handles nulls + && serviceName.equals(((Service) other).serviceName)) // state check + && serviceCost.compareTo(((Service) other).serviceCost) == 0; + } + + @Override + public int hashCode() { + return serviceName.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/contact/UniqueContactList.java b/src/main/java/seedu/address/model/contact/UniqueContactList.java new file mode 100644 index 000000000000..2d6e5eae24e6 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/UniqueContactList.java @@ -0,0 +1,135 @@ +package seedu.address.model.contact; + +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.contact.exceptions.ContactNotFoundException; +import seedu.address.model.contact.exceptions.DuplicateContactException; + +/** + * A list of contacts that enforces uniqueness between its elements and does not allow nulls. + * A contact is considered unique by comparing using {@code Contact#isSameContact(Contact)}. As such, adding and + * updating of contacts uses Contact#isSameContact(Contact) for equality so as to ensure that the contact being added + * or updated is unique in terms of identity in the UniqueContactList. However, the removal of a contact uses + * Contact#equals(Object) so as to ensure that the client with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + * @see Contact#isSameContact(Contact) + */ +public class UniqueContactList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + + /** + * Returns true if the list contains an equivalent contact as the given argument. + */ + public boolean contains(T toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameContact); + } + + /** + * Adds a contact to the list. + * The contact must not already exist in the list. + */ + public void add(T toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateContactException(); + } + internalList.add(toAdd); + } + + /** + * Replaces the contact {@code target} in the list with {@code editedContact}. + * {@code target} must exist in the list. + * The contact identity of {@code editedContact} must not be the same as another existing contact in the list. + */ + public void setContact(T target, T editedContact) { + requireAllNonNull(target, editedContact); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new ContactNotFoundException(); + } + + if (!target.isSameContact(editedContact) && contains(editedContact)) { + throw new DuplicateContactException(); + } + + internalList.set(index, editedContact); + } + + /** + * Removes the equivalent contact from the list. + * The contact must exist in the list. + */ + public void remove(T toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new ContactNotFoundException(); + } + } + + public void setContacts(UniqueContactList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code contacts}. + * {@code contacts} must not contain duplicate contacts. + */ + public void setContacts(List contacts) { + requireAllNonNull(contacts); + if (!contactsAreUnique(contacts)) { + throw new DuplicateContactException(); + } + + internalList.setAll(contacts); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return FXCollections.unmodifiableObservableList(internalList); + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueContactList // instanceof handles nulls + && internalList.equals(((UniqueContactList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code contacts} contains only unique contacts. + */ + private boolean contactsAreUnique(List contacts) { + for (int i = 0; i < contacts.size() - 1; i++) { + for (int j = i + 1; j < contacts.size(); j++) { + if (contacts.get(i).isSameContact(contacts.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/contact/exceptions/ContactNotFoundException.java b/src/main/java/seedu/address/model/contact/exceptions/ContactNotFoundException.java new file mode 100644 index 000000000000..245a34a92c92 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/exceptions/ContactNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.contact.exceptions; + +/** + * Signals that the operation is unable to find the specified client. + */ +public class ContactNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/contact/exceptions/DuplicateContactException.java b/src/main/java/seedu/address/model/contact/exceptions/DuplicateContactException.java new file mode 100644 index 000000000000..4dc1b9effd11 --- /dev/null +++ b/src/main/java/seedu/address/model/contact/exceptions/DuplicateContactException.java @@ -0,0 +1,11 @@ +package seedu.address.model.contact.exceptions; + +/** + * Signals that the operation will result in duplicate Contactss (Contacts are considered duplicates if they have the + * same identity). + */ +public class DuplicateContactException extends RuntimeException { + public DuplicateContactException() { + super("Operation would result in duplicate contacts"); + } +} 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 c9b5868427ca..000000000000 --- 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 557a7a60cd51..000000000000 --- a/src/main/java/seedu/address/model/person/Person.java +++ /dev/null @@ -1,120 +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 of the same name have at least one other identity field that is the same. - * 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()) - && (otherPerson.getPhone().equals(getPhone()) || otherPerson.getEmail().equals(getEmail())); - } - - /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. - */ - @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()) - .append(" Tags: "); - getTags().forEach(builder::append); - return builder.toString(); - } - -} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java deleted file mode 100644 index 5856aa42e6b5..000000000000 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ /dev/null @@ -1,135 +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(); - - /** - * 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 FXCollections.unmodifiableObservableList(internalList); - } - - @Override - public Iterator iterator() { - return internalList.iterator(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof UniquePersonList // instanceof handles nulls - && internalList.equals(((UniquePersonList) other).internalList)); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } - - /** - * Returns true if {@code persons} contains only unique persons. - */ - private boolean personsAreUnique(List persons) { - for (int i = 0; i < persons.size() - 1; i++) { - for (int j = i + 1; j < persons.size(); j++) { - if (persons.get(i).isSamePerson(persons.get(j))) { - return false; - } - } - } - return true; - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java deleted file mode 100644 index fa764426ca73..000000000000 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ /dev/null @@ -1,6 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation is unable to find the specified person. - */ -public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facfa..a7cf70d9374d 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,49 +1,108 @@ package seedu.address.model.util; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; 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.client.Client; +import seedu.address.model.contact.Address; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Service; import seedu.address.model.tag.Tag; +import seedu.address.model.vendor.Vendor; /** * 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"), + public static Contact[] getSamplePersons() { + return new Contact[] { + new Client(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"), + getTagSet("nature"), getServicesMap( + "photographer $2000.00", "hotel $1800.00", "dress $800.00", "ring $10000.00")), + new Client(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"), + getTagSet("fairytale", "elsa"), getServicesMap( + "photographer $1080.00", "hotel $800.00", "catering $3600.00")), + new Client(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"), + getTagSet("pastel", "dreamy"), getServicesMap( + "photographer $1000.00", "hotel $2800.00", "catering $2600.00", "transport $100.00")), + new Client(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"), + getTagSet("disco"), getServicesMap( + "ring $2000.00", "transport $280.00", "invitation $1000.00")), + new Client(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"), + getTagSet("rock"), getServicesMap( + "hotel $400.00", "catering $2000.00")), + new Client(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + getTagSet("colleagues"), getServicesMap( + "photographer $2800.00", "dress $200.00", "transport $100.00")), + new Vendor(new Name("Joe Bros"), new Phone("94311253"), new Email("joebros@example.com"), + new Address("123, Jurong East Ave 6, #08-111"), + getTagSet("western", "italian"), getServicesMap( + "catering $1000.00", "photographer $1200.00")), + new Vendor(new Name("Kim Laces"), new Phone("98762432"), new Email("kimmy@example.com"), + new Address("313, Clementi Ave 5, #02-25"), + getTagSet("nightgown", "headdress"), getServicesMap( + "dress $68.00")), + new Vendor(new Name("Diamond Affair"), new Phone("18762432"), new Email("diamond@example.com"), + new Address("52, Pioneer Ave 5, #02-25"), + getTagSet("diamonds"), getServicesMap( + "hotel $700.00", "ring $500.00", "invitation $100.00")), + new Vendor(new Name("Picture Perfect"), new Phone("93762432"), new Email("pp@example.com"), + new Address("444, River Valley Ave 5, #02-25"), + getTagSet("frames", "portraits"), getServicesMap( + "invitation $288.88", "photographer $1888.00")), + new Vendor(new Name("Deliver 2 Go"), new Phone("98761432"), new Email("d2g@example.com"), + new Address("35, Red Hill Ave 5, #02-25"), + getTagSet("buffet", "limousine"), getServicesMap( + "dress $100.00", "transport $80.00", "catering $2000.00")), + new Vendor(new Name("Foodie Goodie"), new Phone("91232432"), new Email("foodiegoodie@example.com"), + new Address("200, Buona Vista Ave 5, #05-02"), + getTagSet("chinese", "tea"), getServicesMap( + "catering $2800.00", "hotel $300.00")), + new Vendor(new Name("Majestic Suites"), new Phone("98711132"), new Email("majicsuites@example.com"), + new Address("313, Pasir Ris Ave 5, #02-25"), + getTagSet("romantic"), getServicesMap( + "dress $100.00", "hotel $600.00")), + new Vendor(new Name("ClickBait"), new Phone("64427373"), new Email("click@example.com"), + new Address("155, Orchard Ave 3, #03-15"), + getTagSet("printing"), getServicesMap( + "photographer $1500.00", "invitation $100.00")), + new Vendor(new Name("Mandarin Stays"), new Phone("65534222"), new Email("mandarinstays@example.com"), + new Address("225, Marina Road Ave 5"), + getTagSet("attractions"), getServicesMap( + "hotel $588.00")), + new Vendor(new Name("Joseph Stan"), new Phone("98336677"), new Email("jostan@example.com"), + new Address("323, Toa Payoh Ave 8, #05-02"), + getTagSet("personal"), getServicesMap( + "photographer $2000.00")), + new Vendor(new Name("Kcooks"), new Phone("68884888"), new Email("kcookery@example.com"), + new Address("220, Bedok Ave 10, #06-03"), + getTagSet("korean"), getServicesMap( + "catering $2000.00")), + new Vendor(new Name("Jordan Goh"), new Phone("62324111"), new Email("jg@example.com"), + new Address("900, Selangor Ave 2, #11-03"), + getTagSet("finedining"), getServicesMap( + "catering $3500.00")) }; } public static ReadOnlyAddressBook getSampleAddressBook() { AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); + for (Contact sampleContact : getSamplePersons()) { + sampleAb.addContact(sampleContact); } return sampleAb; } @@ -57,4 +116,20 @@ public static Set getTagSet(String... strings) { .collect(Collectors.toSet()); } + /** + * Returns a services map containing the list of strings given. + */ + public static Map getServicesMap(String... strings) { + Map servicesMap = new HashMap<>(); + Arrays.stream(strings) + .map(s -> { + String[] splitString = s.split("\\$"); + String serviceName = splitString[0].trim(); + String serviceCost = splitString[1].trim(); + return new Service(serviceName, serviceCost); + }) + .forEach(s -> servicesMap.put(s.getName(), s)); + return servicesMap; + } + } diff --git a/src/main/java/seedu/address/model/vendor/Vendor.java b/src/main/java/seedu/address/model/vendor/Vendor.java new file mode 100644 index 000000000000..42125486082a --- /dev/null +++ b/src/main/java/seedu/address/model/vendor/Vendor.java @@ -0,0 +1,76 @@ +package seedu.address.model.vendor; + +import java.util.Map; +import java.util.Set; + +import seedu.address.model.ContactType; +import seedu.address.model.contact.Address; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Service; +import seedu.address.model.tag.Tag; + +/** + * Represents a Vendor in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Vendor extends Contact { + private static int vendorId = 1; + + private final int id; + + /** + * Every field must be present and not null. + * + */ + public Vendor(Name name, Phone phone, Email email, Address address, Set tags) { + super(name, phone, email, address, tags); + this.id = vendorId++; + } + + public Vendor(Name name, Phone phone, Email email, Address address, Set tags, + Map service) { + super(name, phone, email, address, tags, service); + this.id = vendorId++; + } + + public Vendor(Name name, Phone phone, Email email, Address address, Set tags, + Map services, int id) { + super(name, phone, email, address, tags, services); + this.id = id; + } + + @Override + public ContactType getType() { + return ContactType.VENDOR; + } + + public int getId() { + return id; + } + + /** + * Returns true if both service providers have the same identity and data fields. + * This defines a stronger notion of equality between two service providers. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Vendor)) { + return false; + } + + Vendor otherVendor = (Vendor) other; + return otherVendor.getName().equals(getName()) + && otherVendor.getPhone().equals(getPhone()) + && otherVendor.getEmail().equals(getEmail()) + && otherVendor.getAddress().equals(getAddress()) + && otherVendor.getTags().equals(getTags()) + && otherVendor.getServices().equals(getServices()); + } +} diff --git a/src/main/java/seedu/address/storage/AccountStorage.java b/src/main/java/seedu/address/storage/AccountStorage.java new file mode 100644 index 000000000000..336ef1c5e7f0 --- /dev/null +++ b/src/main/java/seedu/address/storage/AccountStorage.java @@ -0,0 +1,64 @@ +package seedu.address.storage; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.model.account.Account; +import seedu.address.model.account.AccountList; + +/** + * An interface used to manage storing of Account details. Currently, all Account + * information will be stored locally. + */ +public interface AccountStorage { + /** + * Get the path for storing the Account file. + * @return The local Path that the Account file is stored in. + */ + Path getAccountStorageFilePath(); + + /** + * Easy way of getting AccountList. An AccountList stores all the Account information. + * Throw an exception if the file is not found. + * @return The local AccountList. + * @throws DataConversionException if the data file cannot be converted correctly to username and password. + * @throws IOException if file is not found. + */ + AccountList getAccountList() throws DataConversionException, IOException; + + /** + * Return the AccountList which is the file found in the {@code filePath}. + * Throw an exception if the file is not found. + * @param filePath the path where the local file resides. + * @return The local AccountList. + * @throws DataConversionException if the data file cannot be converted correctly to username and password. + * @throws IOException if the file is not found. + */ + AccountList getAccountList(Path filePath) throws DataConversionException, IOException; + + /** + * Save the {@code account} into the local database. Easy way of calling. + * @param account The newly created account that is to be saved into database. + * @throws IOException if the file or directly cannot be created. + */ + void saveAccount(Account account) throws IOException; + + /** + * Save the {@code account} into the local database specified by {@code filePath}. + * @param account The newly created Account that is to be saved into database. + * @param filePath The Path to save the file in. + * @throws IOException if the file or directory cannot be created. + */ + void saveAccount(Account account, Path filePath) throws IOException; + + /** + * Create a root (default) account such that the very first user can login + * to the system with this root account. + * @param account The root account. + */ + void populateRootAccountIfMissing(Account account); + + void updateAccountPassword(String username, String newPassword) throws FileNotFoundException; +} diff --git a/src/main/java/seedu/address/storage/XmlAccountStorage.java b/src/main/java/seedu/address/storage/XmlAccountStorage.java new file mode 100644 index 000000000000..0b2435491379 --- /dev/null +++ b/src/main/java/seedu/address/storage/XmlAccountStorage.java @@ -0,0 +1,117 @@ +package seedu.address.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.logging.Logger; + +import javax.xml.bind.JAXBException; + +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.XmlUtil; +import seedu.address.model.account.Account; +import seedu.address.model.account.AccountList; + +/** + * A class to access Account data stored as an xml file on the hard disk. + */ +public class XmlAccountStorage implements AccountStorage { + private static final Logger logger = LogsCenter.getLogger(XmlAccountStorage.class); + + private Path accountListPath = Paths.get("data" , "accountlist.xml"); + + public XmlAccountStorage() { + } + + public XmlAccountStorage(Path path) { + accountListPath = path; + } + + @Override + public Path getAccountStorageFilePath() { + return accountListPath; + } + + @Override + public AccountList getAccountList() throws DataConversionException, + FileNotFoundException { + return getAccountList(accountListPath); + } + + @Override + public AccountList getAccountList(Path filePath) throws DataConversionException, + FileNotFoundException { + requireNonNull(filePath); + + if (!Files.exists(filePath)) { + logger.info("Account file " + filePath + " not found."); + throw new FileNotFoundException(); + } + + XmlSerializableAccountList xmlAccount = XmlFileStorage.loadAccountDataFromSaveFile(filePath); + try { + return xmlAccount.toModelType(); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveAccount(Account account) throws IOException { + saveAccount(account, accountListPath); + } + + @Override + public void saveAccount(Account account, Path filePath) throws IOException { + requireNonNull(account); + requireNonNull(filePath); + + if (account.getUserName() == null || account.getUserName().equals("")) { + throw new IllegalArgumentException(Account.USERNAME_CONSTRAINT); + } + try { + AccountList accountList = getAccountList(); + + account.transformToHashedAccount(); + accountList.getList().add(account); + XmlFileStorage.saveAccountDataToFile(filePath, new XmlSerializableAccountList(accountList)); + } catch (DataConversionException e) { + throw new AssertionError("Illegal values found in " + filePath + ": " + e.getMessage()); + } + } + + @Override + public void populateRootAccountIfMissing(Account account) { + requireNonNull(account); + + if (!FileUtil.isFileExists(accountListPath)) { + try { + FileUtil.createFile(accountListPath); + AccountList accountList = new AccountList(); + account.transformToHashedAccount(); + accountList.addAccount(account); + XmlFileStorage.saveAccountDataToFile(accountListPath, new XmlSerializableAccountList(accountList)); + } catch (IOException e) { + logger.info("Account: Unable to create file or directory: " + accountListPath + e.getMessage()); + throw new AssertionError("File not found" + e.getMessage(), e); + } + } + } + + @Override + public void updateAccountPassword(String username, String newPassword) throws FileNotFoundException { + try { + XmlUtil.updatePasswordInFile(accountListPath, username, newPassword); + } catch (JAXBException | IllegalValueException e) { + throw new AssertionError("Unexpected exception " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/seedu/address/storage/XmlAdaptedAccount.java b/src/main/java/seedu/address/storage/XmlAdaptedAccount.java new file mode 100644 index 000000000000..7aa68cdc34b9 --- /dev/null +++ b/src/main/java/seedu/address/storage/XmlAdaptedAccount.java @@ -0,0 +1,84 @@ +package seedu.address.storage; + +import java.util.Objects; + +import javax.xml.bind.annotation.XmlElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.account.Account; +import seedu.address.model.account.Role; + +/** + * JAXB-friendly version of the Client. + */ +public class XmlAdaptedAccount { + @XmlElement(required = true) + private String username; + @XmlElement(required = true) + private String password; + @XmlElement(required = true) + private Role role; + + /** + * Constructs an XmlAdaptedAccount. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAdaptedAccount() {} + + /** + * Constructs an {@code XmlAdaptedAccount} with the given account details. + */ + public XmlAdaptedAccount(String username, String password, Role role) { + this.username = username; + this.password = password; + this.role = role; + } + + /** + * Converts a given account into this class for JAXB use. + * + * @param source the account given + */ + public XmlAdaptedAccount(Account source) { + username = source.getUserName(); + password = source.getPassword(); + role = source.getRole(); + } + + /** + * Converts this jaxb-friendly adapted account object into the model's Account object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted client + */ + public Account toModelType() throws IllegalValueException { + + if (username == null || username.equals("")) { + throw new IllegalValueException(Account.USERNAME_CONSTRAINT); + } + if (password == null || password.equals("")) { + throw new IllegalValueException(Account.PASSWORD_CONSTRAINT); + } + + if (role == null) { + throw new IllegalValueException(Account.ROLE_CONSTRAINT); + } + + return new Account(username, password, role); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlAdaptedAccount)) { + return false; + } + + XmlAdaptedAccount otherAccount = (XmlAdaptedAccount) other; + return Objects.equals(username, otherAccount.username) + && Objects.equals(password, otherAccount.password) + && Objects.equals(role, otherAccount.role); + } +} diff --git a/src/main/java/seedu/address/storage/XmlAdaptedContact.java b/src/main/java/seedu/address/storage/XmlAdaptedContact.java new file mode 100644 index 000000000000..53b6dc8db836 --- /dev/null +++ b/src/main/java/seedu/address/storage/XmlAdaptedContact.java @@ -0,0 +1,179 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.xml.bind.annotation.XmlElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.ContactType; +import seedu.address.model.client.Client; +import seedu.address.model.contact.Address; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Email; +import seedu.address.model.contact.Name; +import seedu.address.model.contact.Phone; +import seedu.address.model.contact.Service; +import seedu.address.model.tag.Tag; +import seedu.address.model.vendor.Vendor; + +/** + * JAXB-friendly version of the Client. + */ +public class XmlAdaptedContact { + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Client's %s field is missing!"; + + @XmlElement(required = true) + private String name; + @XmlElement(required = true) + private String phone; + @XmlElement(required = true) + private String email; + @XmlElement(required = true) + private String address; + @XmlElement(required = true) + private ContactType type; + @XmlElement + private List tagged = new ArrayList<>(); + @XmlElement + private List services = new ArrayList<>(); + + /** + * Constructs an XmlAdaptedContact. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAdaptedContact() { } + + /** + * Constructs an {@code XmlAdaptedContact} with the given client details. + */ + public XmlAdaptedContact(String name, String phone, String email, String address, List tagged, + List services, ContactType type) { + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + if (tagged != null) { + this.tagged = new ArrayList<>(tagged); + } + if (services != null) { + this.services = new ArrayList<>(services); + } + this.type = type; + } + + /** + * Converts a given Client into this class for JAXB use. + * + * @param source future changes to this will not affect the created XmlAdaptedContact + */ + public XmlAdaptedContact(Contact source) { + name = source.getName().fullName; + phone = source.getPhone().value; + email = source.getEmail().value; + address = source.getAddress().value; + tagged = source.getTags().stream() + .map(XmlAdaptedTag::new) + .collect(Collectors.toList()); + services = source.getServicesStream() + .map(XmlAdaptedService::new) + .collect(Collectors.toList()); + type = source.getType(); + } + + /** + * Converts this jaxb-friendly adapted client object into the model's Client object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted client + */ + public Contact toModelType() throws IllegalValueException { + final List contactTags = new ArrayList<>(); + final List contactServices = new ArrayList<>(); + for (XmlAdaptedTag tag : tagged) { + contactTags.add(tag.toModelType()); + } + for (XmlAdaptedService service : services) { + contactServices.add(service.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_NAME_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_PHONE_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_EMAIL_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_ADDRESS_CONSTRAINTS); + } + final Address modelAddress = new Address(address); + + // Additional metadata to determine if contact is a Client or a Vendor + + + final Set modelTags = new HashSet<>(contactTags); + final Map modelServices = new HashMap<>(); + for (Service service : contactServices) { + modelServices.put(service.getName(), service); + } + + if (type == null) { + throw new IllegalValueException("Contact type must be non-null."); + } + + if (type.equals(ContactType.CLIENT)) { + return new Client(modelName, modelPhone, modelEmail, modelAddress, modelTags, modelServices); + } + if (type.equals(ContactType.VENDOR)) { + return new Vendor(modelName, modelPhone, modelEmail, modelAddress, modelTags, modelServices); + } + + throw new IllegalValueException("Illegal contact type. It can only be a client or a vendor."); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlAdaptedContact)) { + return false; + } + + XmlAdaptedContact otherContact = (XmlAdaptedContact) other; + return Objects.equals(name, otherContact.name) + && Objects.equals(phone, otherContact.phone) + && Objects.equals(email, otherContact.email) + && Objects.equals(address, otherContact.address) + && tagged.equals(otherContact.tagged) + && type.equals(otherContact.type) + && services.equals(otherContact.services); + } +} diff --git a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java b/src/main/java/seedu/address/storage/XmlAdaptedPerson.java deleted file mode 100644 index c03785e5700f..000000000000 --- a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java +++ /dev/null @@ -1,137 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import javax.xml.bind.annotation.XmlElement; - -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; - -/** - * JAXB-friendly version of the Person. - */ -public class XmlAdaptedPerson { - - public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; - - @XmlElement(required = true) - private String name; - @XmlElement(required = true) - private String phone; - @XmlElement(required = true) - private String email; - @XmlElement(required = true) - private String address; - - @XmlElement - private List tagged = new ArrayList<>(); - - /** - * Constructs an XmlAdaptedPerson. - * This is the no-arg constructor that is required by JAXB. - */ - public XmlAdaptedPerson() {} - - /** - * Constructs an {@code XmlAdaptedPerson} with the given person details. - */ - public XmlAdaptedPerson(String name, String phone, String email, String address, List tagged) { - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - if (tagged != null) { - this.tagged = new ArrayList<>(tagged); - } - } - - /** - * Converts a given Person into this class for JAXB use. - * - * @param source future changes to this will not affect the created XmlAdaptedPerson - */ - public XmlAdaptedPerson(Person source) { - name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tagged = source.getTags().stream() - .map(XmlAdaptedTag::new) - .collect(Collectors.toList()); - } - - /** - * Converts this jaxb-friendly adapted person object into the model's 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 (XmlAdaptedTag 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_NAME_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_PHONE_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_EMAIL_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_ADDRESS_CONSTRAINTS); - } - final Address modelAddress = new Address(address); - - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - if (!(other instanceof XmlAdaptedPerson)) { - return false; - } - - XmlAdaptedPerson otherPerson = (XmlAdaptedPerson) other; - return Objects.equals(name, otherPerson.name) - && Objects.equals(phone, otherPerson.phone) - && Objects.equals(email, otherPerson.email) - && Objects.equals(address, otherPerson.address) - && tagged.equals(otherPerson.tagged); - } -} diff --git a/src/main/java/seedu/address/storage/XmlAdaptedService.java b/src/main/java/seedu/address/storage/XmlAdaptedService.java new file mode 100644 index 000000000000..444d3c52181b --- /dev/null +++ b/src/main/java/seedu/address/storage/XmlAdaptedService.java @@ -0,0 +1,70 @@ +package seedu.address.storage; + +import javax.xml.bind.annotation.XmlValue; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.contact.Service; + +/** + * JAXB-friendly adapted version of the Service. + */ +public class XmlAdaptedService { + + @XmlValue + private String service; + + /** + * Constructs an XmlAdaptedService. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAdaptedService() {} + + /** + * Constructs a {@code XmlAdaptedService} with the given {@code service}. + */ + public XmlAdaptedService(String service) { + this.service = service; + } + + /** + * Converts a given Service into this class for JAXB use. + * + * @param source future changes to this will not affect the created + */ + public XmlAdaptedService(Service source) { + service = source.toString(); + } + + /** + * Converts this jaxb-friendly adapted service object into the model's Service object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted client + */ + public Service toModelType() throws IllegalValueException { + String[] splitString = service.split("\\$"); + String serviceName = splitString[0].trim(); + String serviceCost = splitString[1].trim(); + + if (!Service.isValidServiceName(serviceName)) { + throw new IllegalValueException(Service.MESSAGE_SERVICE_NAME_CONSTRAINTS); + } + + if (!Service.isValidServiceCost(serviceCost)) { + throw new IllegalValueException(Service.MESSAGE_SERVICE_COST_CONSTRAINTS); + } + return new Service(serviceName, serviceCost); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlAdaptedService)) { + return false; + } + + return service.equals(((XmlAdaptedService) other).service); + } +} diff --git a/src/main/java/seedu/address/storage/XmlAdaptedTag.java b/src/main/java/seedu/address/storage/XmlAdaptedTag.java index d3e2d8be9c4f..f7f2fbee0c1b 100644 --- a/src/main/java/seedu/address/storage/XmlAdaptedTag.java +++ b/src/main/java/seedu/address/storage/XmlAdaptedTag.java @@ -38,7 +38,7 @@ public XmlAdaptedTag(Tag source) { /** * Converts this jaxb-friendly adapted tag object into the model's Tag object. * - * @throws IllegalValueException if there were any data constraints violated in the adapted person + * @throws IllegalValueException if there were any data constraints violated in the adapted client */ public Tag toModelType() throws IllegalValueException { if (!Tag.isValidTagName(tagName)) { diff --git a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java b/src/main/java/seedu/address/storage/XmlAddressBookStorage.java index ecf0e7ec23a8..bf38819691b0 100644 --- a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java +++ b/src/main/java/seedu/address/storage/XmlAddressBookStorage.java @@ -47,7 +47,7 @@ public Optional readAddressBook(Path filePath) throws DataC requireNonNull(filePath); if (!Files.exists(filePath)) { - logger.info("AddressBook file " + filePath + " not found"); + logger.info("Heart² file " + filePath + " not found"); return Optional.empty(); } diff --git a/src/main/java/seedu/address/storage/XmlFileStorage.java b/src/main/java/seedu/address/storage/XmlFileStorage.java index d8f65dc036ab..ded3faced847 100644 --- a/src/main/java/seedu/address/storage/XmlFileStorage.java +++ b/src/main/java/seedu/address/storage/XmlFileStorage.java @@ -36,4 +36,27 @@ public static XmlSerializableAddressBook loadDataFromSaveFile(Path file) throws } } + /** + * Saves the given account data to the specified file. + */ + public static void saveAccountDataToFile(Path file, XmlSerializableAccountList account) + throws FileNotFoundException { + try { + XmlUtil.saveDataToFile(file, account); + } catch (JAXBException e) { + throw new AssertionError("Unexpected exception " + e.getMessage(), e); + } + } + + /** + * Returns account list in the file or an empty account list + */ + public static XmlSerializableAccountList loadAccountDataFromSaveFile(Path file) throws DataConversionException, + FileNotFoundException { + try { + return XmlUtil.getDataFromFile(file, XmlSerializableAccountList.class); + } catch (JAXBException e) { + throw new DataConversionException(e); + } + } } diff --git a/src/main/java/seedu/address/storage/XmlSerializableAccountList.java b/src/main/java/seedu/address/storage/XmlSerializableAccountList.java new file mode 100644 index 000000000000..e92527a55db7 --- /dev/null +++ b/src/main/java/seedu/address/storage/XmlSerializableAccountList.java @@ -0,0 +1,70 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.account.Account; +import seedu.address.model.account.AccountList; + +/** + * An Immutable Account that is serializable to XML format + */ +@XmlRootElement(name = "accountmanager") +public class XmlSerializableAccountList { + + public static final String MESSAGE_DUPLICATE_ACCOUNT = "Database contains duplicate account."; + + @XmlElement + private List accounts; + + /** + * Creates an empty XmlSerializableAddressBook. + * This empty constructor is required for marshalling. + */ + public XmlSerializableAccountList() { + accounts = new ArrayList<>(); + } + + /** + * Conversion + */ + public XmlSerializableAccountList(AccountList src) { + this(); + accounts.addAll(src.getList().stream().map(XmlAdaptedAccount::new).collect(Collectors.toList())); + } + + /** + * Converts this addressbook into the model's {@code AddressBook} object. + * + * @throws IllegalValueException if there were any data constraints violated or duplicates in the + * {@code XmlAdaptedContact}. + */ + public AccountList toModelType() throws IllegalValueException { + AccountList accountList = new AccountList(); + for (XmlAdaptedAccount acc : accounts) { + Account account = acc.toModelType(); + if (accountList.hasAccount(account)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_ACCOUNT); + } + accountList.addAccount(account); + } + return accountList; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlSerializableAccountList)) { + return false; + } + return accounts.equals(((XmlSerializableAccountList) other).accounts); + } +} diff --git a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java b/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java index b85fa4a8f07e..6e65e3fa3a10 100644 --- a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java @@ -10,7 +10,7 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** * An Immutable AddressBook that is serializable to XML format @@ -18,10 +18,10 @@ @XmlRootElement(name = "addressbook") public class XmlSerializableAddressBook { - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate client(s)."; @XmlElement - private List persons; + private List persons; /** * Creates an empty XmlSerializableAddressBook. @@ -36,23 +36,23 @@ public XmlSerializableAddressBook() { */ public XmlSerializableAddressBook(ReadOnlyAddressBook src) { this(); - persons.addAll(src.getPersonList().stream().map(XmlAdaptedPerson::new).collect(Collectors.toList())); + persons.addAll(src.getContactList().stream().map(XmlAdaptedContact::new).collect(Collectors.toList())); } /** * Converts this addressbook into the model's {@code AddressBook} object. * * @throws IllegalValueException if there were any data constraints violated or duplicates in the - * {@code XmlAdaptedPerson}. + * {@code XmlAdaptedContact}. */ public AddressBook toModelType() throws IllegalValueException { AddressBook addressBook = new AddressBook(); - for (XmlAdaptedPerson p : persons) { - Person person = p.toModelType(); - if (addressBook.hasPerson(person)) { + for (XmlAdaptedContact p : persons) { + Contact contact = p.toModelType(); + if (addressBook.hasContact(contact)) { throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); } - addressBook.addPerson(person); + addressBook.addContact(contact); } return addressBook; } diff --git a/src/main/java/seedu/address/ui/BrowserPanel.java b/src/main/java/seedu/address/ui/BrowserPanel.java index b43de90a2b9f..590fa5fdcc52 100644 --- a/src/main/java/seedu/address/ui/BrowserPanel.java +++ b/src/main/java/seedu/address/ui/BrowserPanel.java @@ -1,5 +1,7 @@ package seedu.address.ui; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.logging.Logger; @@ -12,8 +14,9 @@ import javafx.scene.web.WebView; import seedu.address.MainApp; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.ClearBrowserPanelRequestEvent; import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** * The Browser Panel of the App. @@ -22,7 +25,7 @@ public class BrowserPanel extends UiPart { public static final String DEFAULT_PAGE = "default.html"; public static final String SEARCH_PAGE_URL = - "https://se-edu.github.io/addressbook-level4/DummySearchPage.html?name="; + "gallant-hugle-b21445.netlify.com"; private static final String FXML = "BrowserPanel.fxml"; @@ -41,8 +44,17 @@ public BrowserPanel() { registerAsAnEventHandler(this); } - private void loadPersonPage(Person person) { - loadPage(SEARCH_PAGE_URL + person.getName().fullName); + /** + * Loads the page of the person. + * @param contact Contact to be loaded + */ + private void loadPersonPage(Contact contact) { + try { + URI uri = new URI("https", SEARCH_PAGE_URL, "", contact.getUrlContactData(), ""); + loadPage(uri.toString()); + } catch (URISyntaxException exception) { + loadPage("https://" + SEARCH_PAGE_URL); + } } public void loadPage(String url) { @@ -69,4 +81,10 @@ private void handlePersonPanelSelectionChangedEvent(PersonPanelSelectionChangedE logger.info(LogsCenter.getEventHandlingLogMessage(event)); loadPersonPage(event.getNewSelection()); } + + @Subscribe + private void handleClearBrowserPanelEvent(ClearBrowserPanelRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + loadDefaultPage(); + } } diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 3d7aaded5640..fc04ede8126d 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -13,6 +13,7 @@ import seedu.address.logic.Logic; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.commands.exceptions.LackOfPrivilegeException; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -109,7 +110,7 @@ private void handleCommandEntered() { logger.info("Result: " + commandResult.feedbackToUser); raise(new NewResultAvailableEvent(commandResult.feedbackToUser)); - } catch (CommandException | ParseException e) { + } catch (CommandException | ParseException | LackOfPrivilegeException e) { initHistory(); // handle command failure setStyleToIndicateCommandFailure(); diff --git a/src/main/java/seedu/address/ui/LoginWindow.java b/src/main/java/seedu/address/ui/LoginWindow.java new file mode 100644 index 000000000000..4ff5693a5237 --- /dev/null +++ b/src/main/java/seedu/address/ui/LoginWindow.java @@ -0,0 +1,149 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import seedu.address.commons.core.Config; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.ExitAppRequestEvent; +import seedu.address.commons.events.ui.LoginSuccessEvent; +import seedu.address.commons.events.ui.NewResultAvailableEvent; +import seedu.address.logic.ListElementPointer; +import seedu.address.logic.Logic; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.commands.exceptions.LackOfPrivilegeException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.UserPrefs; + +/** + * The Login Window. Users have to login before they are redirected to the Main Window. + */ +public class LoginWindow extends UiPart { + + public static final String ERROR_STYLE_CLASS = "error"; + private static final String FXML = "LoginWindow.fxml"; + + private final Logger logger = LogsCenter.getLogger(LoginWindow.class); + + private Stage loginStage; + private Config config; + private Logic logic; + private UserPrefs prefs; + private ListElementPointer historySnapshot; + + @FXML + private Label statusPlaceholder; + + @FXML + private TextField loginCli; + + /** + * Instantiates the Login Window + * @param logic Logic parsed from UiManager + */ + public LoginWindow(Stage loginStage, Config config, UserPrefs prefs, Logic logic) { + super(FXML, loginStage); + + // Set dependencies + this.logic = logic; + this.loginStage = loginStage; + this.prefs = prefs; + this.config = config; + historySnapshot = logic.getHistorySnapshot(); + + setWindowDefaultSize(prefs); + } + + /** + * Sets the default size based on user preferences. + */ + private void setWindowDefaultSize(UserPrefs prefs) { + loginStage.setHeight(prefs.getGuiSettings().getWindowHeight()); + loginStage.setWidth(prefs.getGuiSettings().getWindowWidth()); + if (prefs.getGuiSettings().getWindowCoordinates() != null) { + loginStage.setX(prefs.getGuiSettings().getWindowCoordinates().getX()); + loginStage.setY(prefs.getGuiSettings().getWindowCoordinates().getY()); + } + } + + void show() { + loginStage.show(); + } + + /** + * Handles the Enter button pressed event. + */ + @FXML + private void handleLogin() { + try { + CommandResult commandResult = logic.execute(loginCli.getText()); + initHistory(); + historySnapshot.next(); + // process result of the command + loginCli.setText(""); + logger.info("Result: " + commandResult.feedbackToUser); + raise(new NewResultAvailableEvent(commandResult.feedbackToUser)); + statusPlaceholder.setText(commandResult.feedbackToUser); + + // If the login was successful, raise a new login success event and hide the login window. + if (commandResult.feedbackToUser.equals("Successfully logged in.")) { + raise(new LoginSuccessEvent()); + hide(); + } + + } catch (CommandException | ParseException | LackOfPrivilegeException e) { + initHistory(); + // handle command failure + setStyleToIndicateCommandFailure(); + logger.info("Invalid command: " + loginCli.getText()); + statusPlaceholder.setText(e.getMessage()); + } + } + + /** + * Initializes the history snapshot. + */ + private void initHistory() { + historySnapshot = logic.getHistorySnapshot(); + // add an empty string to represent the most-recent end of historySnapshot, to be shown to + // the user if she tries to navigate past the most-recent end of the historySnapshot. + historySnapshot.add(""); + } + + /** + * Closes the application. + */ + @FXML + private void handleExit() { + raise(new ExitAppRequestEvent()); + } + + /** + * Sets the command box style to use the default style. + */ + private void setStyleToDefault() { + loginCli.getStyleClass().remove(ERROR_STYLE_CLASS); + } + + /** + * Sets the command box style to indicate a failed command. + */ + private void setStyleToIndicateCommandFailure() { + ObservableList styleClass = loginCli.getStyleClass(); + + if (styleClass.contains(ERROR_STYLE_CLASS)) { + return; + } + + styleClass.add(ERROR_STYLE_CLASS); + } + + private void hide() { + loginStage.hide(); + } +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 0e361a4d7baf..9b3e586fb595 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -1,9 +1,15 @@ package seedu.address.ui; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; import java.util.logging.Logger; import com.google.common.eventbus.Subscribe; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.MenuItem; @@ -15,10 +21,16 @@ import seedu.address.commons.core.Config; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.DisplayAutoMatchResultRequestEvent; import seedu.address.commons.events.ui.ExitAppRequestEvent; +import seedu.address.commons.events.ui.LoginSuccessEvent; +import seedu.address.commons.events.ui.LogoutRequestEvent; import seedu.address.commons.events.ui.ShowHelpRequestEvent; import seedu.address.logic.Logic; +import seedu.address.model.AutoMatchResult; import seedu.address.model.UserPrefs; +import seedu.address.model.contact.Contact; +import seedu.address.model.contact.Service; /** * The Main Window. Provides the basic application layout containing @@ -31,14 +43,25 @@ public class MainWindow extends UiPart { private final Logger logger = LogsCenter.getLogger(getClass()); private Stage primaryStage; + private Stage loginStage; private Logic logic; + private boolean hasFilledParts; // Independent Ui parts residing in this Ui container private BrowserPanel browserPanel; private PersonListPanel personListPanel; + private ServiceListPanel photoListPanel; + private ServiceListPanel hotelListPanel; + private ServiceListPanel cateringListPanel; + private ServiceListPanel dressListPanel; + private ServiceListPanel ringListPanel; + private ServiceListPanel transportListPanel; + private ServiceListPanel invitationListPanel; private Config config; private UserPrefs prefs; private HelpWindow helpWindow; + private LoginWindow loginWindow; + private StatusBarFooter statusBarFooter; @FXML private StackPane browserPlaceholder; @@ -52,6 +75,27 @@ public class MainWindow extends UiPart { @FXML private StackPane personListPanelPlaceholder; + @FXML + private StackPane photoListPanelPlaceholder; + + @FXML + private StackPane hotelListPanelPlaceholder; + + @FXML + private StackPane cateringListPanelPlaceholder; + + @FXML + private StackPane dressListPanelPlaceholder; + + @FXML + private StackPane ringListPanelPlaceholder; + + @FXML + private StackPane transportListPanelPlaceholder; + + @FXML + private StackPane invitationListPanelPlaceholder; + @FXML private StackPane resultDisplayPlaceholder; @@ -70,11 +114,13 @@ public MainWindow(Stage primaryStage, Config config, UserPrefs prefs, Logic logi // Configure the UI setTitle(config.getAppTitle()); setWindowDefaultSize(prefs); + primaryStage.centerOnScreen(); setAccelerators(); registerAsAnEventHandler(this); helpWindow = new HelpWindow(); + hasFilledParts = false; } public Stage getPrimaryStage() { @@ -115,6 +161,17 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { }); } + /** + * Creates a new login stage and displays the login window + */ + void displayLoginWindow() { + // Create new login stage for login window + loginStage = new Stage(); + loginStage.centerOnScreen(); + loginWindow = new LoginWindow(loginStage, config, prefs, logic); + loginStage.show(); + } + /** * Fills up all the placeholders of this window. */ @@ -122,17 +179,90 @@ void fillInnerParts() { browserPanel = new BrowserPanel(); browserPlaceholder.getChildren().add(browserPanel.getRoot()); + // Fill up person list personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + // Show display ResultDisplay resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(prefs.getAddressBookFilePath()); + // Show status bar + statusBarFooter = new StatusBarFooter(prefs.getAddressBookFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); + // Show command box CommandBox commandBox = new CommandBox(logic); commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + + // Set to true that parts have been initialised. + hasFilledParts = true; + } + + /** + * Handles logout event + * Hides main window and displays login window. + */ + public void handleLogout() { + hide(); + displayLoginWindow(); + } + + /** + * Sieves out the contacts based on the service type. + * @param resultMap Map of all related contacts. + * @param serviceType Type of service. + * @return List of Contacts of this service type. + */ + private ObservableList getfilteredServiceList( + Map> resultMap, String serviceType) { + List serviceList = new ArrayList<>(); + resultMap.forEach((( + contact, temp) -> resultMap.get(contact) + .forEach(service -> { + if (service.getName().equals(serviceType)) { + serviceList.add(contact); + } + }))); + return FXCollections.observableList(serviceList); + } + + /** + * Handles the display of suitable service providers for the client using the automatch results + */ + public void showAutoMatchDisplay() { + AutoMatchResult autoMatchResult = logic.getAutoMatchResult(); + Map> resultMap = autoMatchResult.getContactAndServicesMap(); + + ObservableList photographerList = getfilteredServiceList(resultMap, "photographer"); + ObservableList hotelList = getfilteredServiceList(resultMap, "hotel"); + ObservableList cateringList = getfilteredServiceList(resultMap, "catering"); + ObservableList dressList = getfilteredServiceList(resultMap, "dress"); + ObservableList ringList = getfilteredServiceList(resultMap, "ring"); + ObservableList transportList = getfilteredServiceList(resultMap, "transport"); + ObservableList invitationList = getfilteredServiceList(resultMap, "invitation"); + + // TODO Fill up each service panel + photoListPanel = new ServiceListPanel(photographerList, "photographer"); + photoListPanelPlaceholder.getChildren().add(photoListPanel.getRoot()); + + hotelListPanel = new ServiceListPanel(hotelList, "hotel"); + hotelListPanelPlaceholder.getChildren().add(hotelListPanel.getRoot()); + + cateringListPanel = new ServiceListPanel(cateringList, "catering"); + cateringListPanelPlaceholder.getChildren().add(cateringListPanel.getRoot()); + + dressListPanel = new ServiceListPanel(dressList, "dress"); + dressListPanelPlaceholder.getChildren().add(dressListPanel.getRoot()); + + ringListPanel = new ServiceListPanel(ringList, "ring"); + ringListPanelPlaceholder.getChildren().add(ringListPanel.getRoot()); + + transportListPanel = new ServiceListPanel(transportList, "transport"); + transportListPanelPlaceholder.getChildren().add(transportListPanel.getRoot()); + + invitationListPanel = new ServiceListPanel(invitationList, "invitation"); + invitationListPanelPlaceholder.getChildren().add(invitationListPanel.getRoot()); } void hide() { @@ -176,6 +306,7 @@ public void handleHelp() { } void show() { + logger.fine("Showing main window."); primaryStage.show(); } @@ -192,12 +323,38 @@ public PersonListPanel getPersonListPanel() { } void releaseResources() { - browserPanel.freeResources(); + if (browserPanel != null) { + browserPanel.freeResources(); + } } + @Subscribe + private void handleLoginSuccessEvent(LoginSuccessEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + show(); + if (!hasFilledParts) { + fillInnerParts(); + } else { + statusBarFooter.setUsernameStatus("username: " + UserPrefs.getUsernameAndRoleToDisplay()); + } + } + + @Subscribe private void handleShowHelpEvent(ShowHelpRequestEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); handleHelp(); } + + @Subscribe + private void handleLogoutEvent(LogoutRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + handleLogout(); + } + + @Subscribe + private void handleAutoMatchEvent(DisplayAutoMatchResultRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + showAutoMatchDisplay(); + } } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index f6727ea83abd..bef7d29bc277 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -5,10 +5,10 @@ import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** - * An UI component that displays information of a {@code Person}. + * An UI component that displays information of a {@code Client}. */ public class PersonCard extends UiPart { @@ -22,7 +22,7 @@ public class PersonCard extends UiPart { * @see The issue on AddressBook level 4 */ - public final Person person; + public final Contact contact; @FXML private HBox cardPane; @@ -39,15 +39,46 @@ public class PersonCard extends UiPart { @FXML private FlowPane tags; - public PersonCard(Person person, int displayedIndex) { + public PersonCard(Contact contact, 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().forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + this.contact = contact; + id.setText("#" + displayedIndex + ". "); + name.setText(contact.getName().fullName); + phone.setText(contact.getPhone().value); + address.setText(contact.getAddress().value); + email.setText(contact.getEmail().value); + assignTags(contact); + } + + /** + * Assigns all tags for the client with a label. + * + * @param contact Current client to assign tags to + */ + private void assignTags(Contact contact) { + contact.getTags().forEach(tag -> { + Label tagLabel = createLabelforTag(tag.tagName); + tags.getChildren().add(tagLabel); + }); + } + + /** + * Creates a label for the tag. Label is set to grey if the tag indicates price. + * Otherwise, it would be set to pink as default. + * + * @param tagName Name of the tag + * @return new Label for the tag + */ + private Label createLabelforTag(String tagName) { + Label tagLabel = new Label(tagName); + + if (tagName.toLowerCase().contains("price")) { + tagLabel.getStyleClass().add("grey"); + } else { + tagLabel.getStyleClass().add("pink"); + } + + return tagLabel; } @Override @@ -65,6 +96,6 @@ public boolean equals(Object other) { // state check PersonCard card = (PersonCard) other; return id.getText().equals(card.id.getText()) - && person.equals(card.person); + && contact.equals(card.contact); } } diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index 80080adb4305..8b36f07c44af 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -10,10 +10,13 @@ import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.layout.Region; +import seedu.address.commons.core.EventsCenter; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.ClearBrowserPanelRequestEvent; +import seedu.address.commons.events.ui.DeselectRequestEvent; import seedu.address.commons.events.ui.JumpToListRequestEvent; import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; -import seedu.address.model.person.Person; +import seedu.address.model.contact.Contact; /** * Panel containing the list of persons. @@ -23,16 +26,16 @@ public class PersonListPanel extends UiPart { private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); @FXML - private ListView personListView; + private ListView personListView; - public PersonListPanel(ObservableList personList) { + public PersonListPanel(ObservableList contactList) { super(FXML); - setConnections(personList); + setConnections(contactList); registerAsAnEventHandler(this); } - private void setConnections(ObservableList personList) { - personListView.setItems(personList); + private void setConnections(ObservableList contactList) { + personListView.setItems(contactList); personListView.setCellFactory(listView -> new PersonListViewCell()); setEventHandlerForSelectionChangeEvent(); } @@ -41,7 +44,7 @@ private void setEventHandlerForSelectionChangeEvent() { personListView.getSelectionModel().selectedItemProperty() .addListener((observable, oldValue, newValue) -> { if (newValue != null) { - logger.fine("Selection in person list panel changed to : '" + newValue + "'"); + logger.fine("Selection in client list panel changed to : '" + newValue + "'"); raise(new PersonPanelSelectionChangedEvent(newValue)); } }); @@ -63,19 +66,25 @@ private void handleJumpToListRequestEvent(JumpToListRequestEvent event) { scrollTo(event.targetIndex); } + @Subscribe + private void handleDeselectRequestEvent(DeselectRequestEvent event) { + Platform.runLater(() -> personListView.getSelectionModel().clearSelection()); + EventsCenter.getInstance().post(new ClearBrowserPanelRequestEvent()); + } + /** - * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. + * Custom {@code ListCell} that displays the graphics of a {@code Client} using a {@code PersonCard}. */ - class PersonListViewCell extends ListCell { + class PersonListViewCell extends ListCell { @Override - protected void updateItem(Person person, boolean empty) { - super.updateItem(person, empty); + protected void updateItem(Contact contact, boolean empty) { + super.updateItem(contact, empty); - if (empty || person == null) { + if (empty || contact == null) { setGraphic(null); setText(null); } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); + setGraphic(new PersonCard(contact, contact.getId()).getRoot()); } } } diff --git a/src/main/java/seedu/address/ui/ServiceCard.java b/src/main/java/seedu/address/ui/ServiceCard.java new file mode 100644 index 000000000000..a396c23249b4 --- /dev/null +++ b/src/main/java/seedu/address/ui/ServiceCard.java @@ -0,0 +1,60 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.contact.Contact; + +/** + * An UI component that displays information of a service provided by a {@code Vendor}. + */ +public class ServiceCard extends UiPart { + + private static final String FXML = "ServiceListCard.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 Contact contact; + + @FXML + private HBox cardPane; + @FXML + private Label name; + @FXML + private Label id; + @FXML + private Label price; + + public ServiceCard(Contact contact, String serviceType) { + super(FXML); + this.contact = contact; + id.setText("#" + contact.getId() + ". "); + name.setText(contact.getName().fullName); + price.setText("Cost: $" + contact.getServices().get(serviceType).getCost().toPlainString()); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ServiceCard)) { + return false; + } + + // state check + ServiceCard card = (ServiceCard) other; + return id.getText().equals(card.id.getText()) + && contact.equals(card.contact); + } +} diff --git a/src/main/java/seedu/address/ui/ServiceListPanel.java b/src/main/java/seedu/address/ui/ServiceListPanel.java new file mode 100644 index 000000000000..495a1babc418 --- /dev/null +++ b/src/main/java/seedu/address/ui/ServiceListPanel.java @@ -0,0 +1,58 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.contact.Contact; + +/** + * Panel containing the list of persons. + */ +public class ServiceListPanel extends UiPart { + private static final String FXML = "ServiceListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ServiceListPanel.class); + private final String serviceType; + + @FXML + private ListView serviceListView; + + public ServiceListPanel(ObservableList contactList, String serviceType) { + super(FXML); + setConnections(contactList); + registerAsAnEventHandler(this); + this.serviceType = serviceType; + } + + private void setConnections(ObservableList contactList) { + serviceListView.setItems(contactList); + serviceListView.setCellFactory(listView -> new ServiceListViewCell()); + + // Disable selection from table + serviceListView.setMouseTransparent(true); + serviceListView.setFocusTraversable(false); + } + + /** + * Custom {@code ListCell} that displays the graphics of a service provided by the {@code Vendor} + * using a {@code ServiceCard}. + */ + class ServiceListViewCell extends ListCell { + @Override + protected void updateItem(Contact contact, boolean empty) { + super.updateItem(contact, empty); + + if (empty || contact == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ServiceCard(contact, serviceType).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java index f6ba29502422..0f06fa4bee35 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/StatusBarFooter.java @@ -15,6 +15,7 @@ import javafx.scene.layout.Region; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.model.UserPrefs; /** * A ui for the status bar that is displayed at the footer of the application. @@ -42,6 +43,8 @@ public class StatusBarFooter extends UiPart { private StatusBar syncStatus; @FXML private StatusBar saveLocationStatus; + @FXML + private StatusBar usernameStatus; public StatusBarFooter(Path saveLocation) { @@ -49,6 +52,7 @@ public StatusBarFooter(Path saveLocation) { setSyncStatus(SYNC_STATUS_INITIAL); setSaveLocation(Paths.get(".").resolve(saveLocation).toString()); registerAsAnEventHandler(this); + setUsernameStatus("username: " + UserPrefs.getUsernameAndRoleToDisplay()); } /** @@ -73,6 +77,10 @@ private void setSyncStatus(String status) { Platform.runLater(() -> syncStatus.setText(status)); } + public void setUsernameStatus(String status) { + Platform.runLater(() -> usernameStatus.setText(status)); + } + @Subscribe public void handleAddressBookChangedEvent(AddressBookChangedEvent abce) { long now = clock.millis(); diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/seedu/address/ui/Ui.java index e6a67fe8c027..4bdb4b1e71c5 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/seedu/address/ui/Ui.java @@ -13,4 +13,7 @@ public interface Ui { /** Stops the UI. */ void stop(); + /** Registers user as logged in. */ + void commitUserLoggedInSuccessfully(); + } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index 3fd3c17be156..e790f2abd844 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -14,6 +14,7 @@ import seedu.address.commons.core.Config; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.storage.DataSavingExceptionEvent; +import seedu.address.commons.events.ui.LoginSuccessEvent; import seedu.address.commons.util.StringUtil; import seedu.address.logic.Logic; import seedu.address.model.UserPrefs; @@ -30,13 +31,14 @@ public class UiManager extends ComponentManager implements Ui { public static final String FILE_OPS_ERROR_DIALOG_CONTENT_MESSAGE = "Could not save data to file"; 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/heart2_logo_transparent.png"; private Logic logic; private Config config; private UserPrefs prefs; private MainWindow mainWindow; + private boolean hasLoggedIn = false; + public UiManager(Logic logic, Config config, UserPrefs prefs) { super(); this.logic = logic; @@ -54,7 +56,12 @@ public void start(Stage primaryStage) { try { mainWindow = new MainWindow(primaryStage, config, prefs, logic); mainWindow.show(); //This should be called before creating other UI parts - mainWindow.fillInnerParts(); + if (hasLoggedIn) { + raise(new LoginSuccessEvent()); + } else { + mainWindow.hide(); + mainWindow.displayLoginWindow(); + } } catch (Throwable e) { logger.severe(StringUtil.getDetails(e)); @@ -62,6 +69,13 @@ public void start(Stage primaryStage) { } } + /** + * Registers user as logged in + */ + public void commitUserLoggedInSuccessfully() { + hasLoggedIn = true; + } + @Override public void stop() { prefs.updateLastUsedGuiSetting(mainWindow.getCurrentGuiSetting()); @@ -89,7 +103,7 @@ void showAlertDialogAndWait(Alert.AlertType type, String title, String headerTex private static void showAlertDialogAndWait(Stage owner, AlertType type, String title, String headerText, String contentText) { final Alert alert = new Alert(type); - alert.getDialogPane().getStylesheets().add("view/DarkTheme.css"); + alert.getDialogPane().getStylesheets().add("view/MainTheme.css"); alert.initOwner(owner); alert.setTitle(title); alert.setHeaderText(headerText); diff --git a/src/main/resources/images/heart2_logo.png b/src/main/resources/images/heart2_logo.png new file mode 100644 index 000000000000..1d27fb6e46f8 Binary files /dev/null and b/src/main/resources/images/heart2_logo.png differ diff --git a/src/main/resources/images/heart2_logo_transparent.png b/src/main/resources/images/heart2_logo_transparent.png new file mode 100644 index 000000000000..4564b5dbd8de Binary files /dev/null and b/src/main/resources/images/heart2_logo_transparent.png differ diff --git a/src/main/resources/images/ic_catering.png b/src/main/resources/images/ic_catering.png new file mode 100644 index 000000000000..296de428b935 Binary files /dev/null and b/src/main/resources/images/ic_catering.png differ diff --git a/src/main/resources/images/ic_dress.png b/src/main/resources/images/ic_dress.png new file mode 100644 index 000000000000..b42f5d9aa085 Binary files /dev/null and b/src/main/resources/images/ic_dress.png differ diff --git a/src/main/resources/images/ic_hotel.png b/src/main/resources/images/ic_hotel.png new file mode 100644 index 000000000000..97a1e17929d3 Binary files /dev/null and b/src/main/resources/images/ic_hotel.png differ diff --git a/src/main/resources/images/ic_invitation.png b/src/main/resources/images/ic_invitation.png new file mode 100644 index 000000000000..67487eaacbcf Binary files /dev/null and b/src/main/resources/images/ic_invitation.png differ diff --git a/src/main/resources/images/ic_photographer.png b/src/main/resources/images/ic_photographer.png new file mode 100644 index 000000000000..315e373b58ca Binary files /dev/null and b/src/main/resources/images/ic_photographer.png differ diff --git a/src/main/resources/images/ic_ring.png b/src/main/resources/images/ic_ring.png new file mode 100644 index 000000000000..d22866a1ae6d Binary files /dev/null and b/src/main/resources/images/ic_ring.png differ diff --git a/src/main/resources/images/ic_transport.png b/src/main/resources/images/ic_transport.png new file mode 100644 index 000000000000..6f76a995a174 Binary files /dev/null and b/src/main/resources/images/ic_transport.png differ diff --git a/src/main/resources/view/BrowserPanel.fxml b/src/main/resources/view/BrowserPanel.fxml index 31670827e3da..a3e87d3a8d35 100644 --- a/src/main/resources/view/BrowserPanel.fxml +++ b/src/main/resources/view/BrowserPanel.fxml @@ -2,7 +2,6 @@ - diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 70bd59ab3215..4f9bfe90b8e8 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -2,7 +2,6 @@ - diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index c07e8e685014..8b5209559c71 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -1,10 +1,10 @@ - + + - - + diff --git a/src/main/resources/view/LoginWindow.fxml b/src/main/resources/view/LoginWindow.fxml new file mode 100644 index 000000000000..b7a9b7e97c31 --- /dev/null +++ b/src/main/resources/view/LoginWindow.fxml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/MainTheme.css similarity index 95% rename from src/main/resources/view/DarkTheme.css rename to src/main/resources/view/MainTheme.css index c8941ea18263..867d3c4974c8 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/MainTheme.css @@ -173,6 +173,11 @@ -fx-text-fill: white; } +.titled-pane > .title { + -fx-background-color: derive(#f7d2e0, 30%); + -fx-alignment: top-center; +} + .grid-pane { -fx-background-color: derive(#1d1d1d, 30%); -fx-border-color: derive(#1d1d1d, 30%); @@ -319,9 +324,10 @@ #commandTextField { -fx-background-color: transparent #383838 transparent #383838; -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; + -fx-border-color: #F7D2E0 #F7D2E0 #ffffff #F7D2E0; -fx-border-insets: 0; -fx-border-width: 1; + -fx-border-radius: 20; -fx-font-family: "Segoe UI Light"; -fx-font-size: 13pt; -fx-text-fill: white; @@ -342,10 +348,18 @@ } #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; } + +#tags .pink { + -fx-text-fill: black; + -fx-background-color: #ffcfd4; +} + +#tags .grey { + -fx-text-fill: black; + -fx-background-color: #d0d0d0; +} diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index daf386d8f5b8..9183b9a25a8a 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -2,67 +2,168 @@ - + + + + - + + - - - - - - - - - + minWidth="1000" minHeight="700" onCloseRequest="#handleExit"> + + + + + + + + + + + + +

+ + + + + + + + + + + + - - - - - - - - - + + + + + - - - - - + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + - - - - + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f08ea32ad558..ec9587e16bac 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -8,7 +8,6 @@ - diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml index 8836d323cc5d..074eb3e9635e 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/PersonListPanel.fxml @@ -2,7 +2,6 @@ - diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 58d5ad3dc56c..61bc0918ddbb 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -2,8 +2,7 @@ - + xmlns:fx="http://javafx.com/fxml/1">