diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 6ff220b5196..602aaef1655 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -26,6 +26,14 @@ jobs: working-directory: ${{ github.workspace }}/.github run: ./run-checks.sh + - name: Set up Xvfb + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y xvfb + Xvfb :99 -screen 0 1280x1024x24 & + echo "DISPLAY=:99" >> $GITHUB_ENV + - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..1d6057ac0a6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /.gradle/ /build/ src/main/resources/docs/ +/bin/ # IDEA files /.idea/ diff --git a/README.md b/README.md index 13f5c77403f..e034b5177b3 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) - +[![Java CI](https://github.com/AY2324S2-CS2103T-T16-2/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2324S2-CS2103T-T16-2/tp/actions/workflows/gradle.yml) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. +# FriendFolio + +## Description + +* The project simulates an ongoing software project for a desktop application (called _FriendFolio_) used for managing + contact details. + * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) + than what students usually write in beginner-level SE modules, without being overwhelmingly big. + * It comes with a **reasonable level of user and developer documentation**. + +## Useful Links + +* FriendFolio's [Main Website Link](https://ay2324s2-cs2103t-t16-2.github.io/tp/) +* FriendFolio's [User Guide Link](https://ay2324s2-cs2103t-t16-2.github.io/tp/UserGuide.html) +* FriendFolio's [Developer Guide Link](https://ay2324s2-cs2103t-t16-2.github.io/tp/DeveloperGuide.html) diff --git a/build.gradle b/build.gradle index a2951cc709e..334cbc173ca 100644 --- a/build.gradle +++ b/build.gradle @@ -59,14 +59,21 @@ dependencies { implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' + implementation group: 'com.google.zxing', name: 'core', version: '3.3.2' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion + testImplementation 'org.testfx:testfx-core:4.0.16-alpha' + testImplementation 'org.testfx:testfx-junit5:4.0.16-alpha' } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'friendfolio.jar' } defaultTasks 'clean', 'test' + +run { + enableAssertions = true +} diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..1cd4c2726aa 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,38 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Lim Zhekai - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/zhekaiii)] -* Role: Project Advisor - -### Jane Doe - - - -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] - -* Role: Team Lead -* Responsibilities: UI +* Role: Developer +* Responsibilities: Birthday, Pay Command, Sort Command -### Johnny Doe +### Alvin Ng - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/alvinnzz)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Money Owed, Split Command, Lend Command -### Jean Doe +### Oon Jie Rui - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/jerryo3)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Filter Command, Days Available -### James Doe +### Newton Koh - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/newtonkoh)] * Role: Developer -* Responsibilities: UI +* Responsibilities: UI, Remark Command diff --git a/docs/DevOps.md b/docs/DevOps.md index d2fd91a6001..2104ae105f7 100644 --- a/docs/DevOps.md +++ b/docs/DevOps.md @@ -4,22 +4,22 @@ title: DevOps guide --- * Table of Contents -{:toc} + {:toc} -------------------------------------------------------------------------------------------------------------------- ## Build automation -This project uses Gradle for **build automation and dependency management**. **You are recommended to read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html)**. - +This project uses Gradle for **build automation and dependency management**. **You are recommended to +read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html)**. Given below are how to use Gradle for some important project tasks. - * **`clean`**: Deletes the files created during the previous build tasks (e.g. files in the `build` folder).
e.g. `./gradlew clean` -* **`shadowJar`**: Uses the ShadowJar plugin to creat a fat JAR file in the `build/lib` folder, *if the current file is outdated*.
+* **`shadowJar`**: Uses the ShadowJar plugin to creat a fat JAR file in the `build/lib` folder, *if the current file is + outdated*.
e.g. `./gradlew shadowJar`. * **`run`**: Builds and runs the application.
@@ -29,28 +29,39 @@ Given below are how to use Gradle for some important project tasks. **`checkstyleTest`**: Runs the code style check for the test code base. * **`test`**: Runs all tests. - * `./gradlew test` — Runs all tests - * `./gradlew clean test` — Cleans the project and runs tests + * `./gradlew test`— Runs all tests + * `./gradlew clean test`— Cleans the project and runs tests -------------------------------------------------------------------------------------------------------------------- ## Continuous integration (CI) -This project uses GitHub Actions for CI. The project comes with the necessary GitHub Actions configurations files (in the `.github/workflows` folder). No further setting up required. +This project uses GitHub Actions for CI. The project comes with the necessary GitHub Actions configurations files (in +the `.github/workflows` folder). No further setting up required. ### Code coverage -As part of CI, this project uses Codecov to generate coverage reports. When CI runs, it will generate code coverage data (based on the tests run by CI) and upload that data to the CodeCov website, which in turn can provide you more info about the coverage of your tests. +As part of CI, this project uses Codecov to generate coverage reports. When CI runs, it will generate code coverage +data (based on the tests run by CI) and upload that data to the CodeCov website, which in turn can provide you more info +about the coverage of your tests. -However, because Codecov is known to run into intermittent problems (e.g., report upload fails) due to issues on the Codecov service side, the CI is configured to pass even if the Codecov task failed. Therefore, developers are advised to check the code coverage levels periodically and take corrective actions if the coverage level falls below desired levels. +However, because Codecov is known to run into intermittent problems (e.g., report upload fails) due to issues on the +Codecov service side, the CI is configured to pass even if the Codecov task failed. Therefore, developers are advised to +check the code coverage levels periodically and take corrective actions if the coverage level falls below desired +levels. -To enable Codecov for forks of this project, follow the steps given in [this se-edu guide](https://se-education.org/guides/tutorials/codecov.html). +To enable Codecov for forks of this project, follow the steps given +in [this se-edu guide](https://se-education.org/guides/tutorials/codecov.html). ### Repository-wide checks -In addition to running Gradle checks, CI includes some repository-wide checks. Unlike the Gradle checks which only cover files used in the build process, these repository-wide checks cover all files in the repository. They check for repository rules which are hard to enforce on development machines such as line ending requirements. +In addition to running Gradle checks, CI includes some repository-wide checks. Unlike the Gradle checks which only cover +files used in the build process, these repository-wide checks cover all files in the repository. They check for +repository rules which are hard to enforce on development machines such as line ending requirements. -These checks are implemented as POSIX shell scripts, and thus can only be run on POSIX-compliant operating systems such as macOS and Linux. To run all checks locally on these operating systems, execute the following in the repository root directory: +These checks are implemented as POSIX shell scripts, and thus can only be run on POSIX-compliant operating systems such +as macOS and Linux. To run all checks locally on these operating systems, execute the following in the repository root +directory: `./config/travis/run-checks.sh` @@ -58,12 +69,14 @@ Any warnings or errors will be printed out to the console. **If adding new checks:** -* Checks are implemented as executable `check-*` scripts within the `.github` directory. The `run-checks.sh` script will automatically pick up and run files named as such. That is, you can add more such files if you need and the CI will do the rest. +* Checks are implemented as executable `check-*` scripts within the `.github` directory. The `run-checks.sh` script will + automatically pick up and run files named as such. That is, you can add more such files if you need and the CI will do + the rest. * Check scripts should print out errors in the format `SEVERITY:FILENAME:LINE: MESSAGE` - * SEVERITY is either ERROR or WARN. - * FILENAME is the path to the file relative to the current directory. - * LINE is the line of the file where the error occurred and MESSAGE is the message explaining the error. + * SEVERITY is either ERROR or WARN. + * FILENAME is the path to the file relative to the current directory. + * LINE is the line of the file where the error occurred and MESSAGE is the message explaining the error. * Check scripts must exit with a non-zero exit code if any errors occur. @@ -73,7 +86,9 @@ Any warnings or errors will be printed out to the console. Here are the steps to create a new release. -1. Update the version number in [`MainApp.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java). +1. Update the version number + in [`MainApp.java`](https://github.com/AY2324S2-CS2103T-T16-2/tp/tree/master/src/main/java/seedu/address/MainApp.java). 1. Generate a fat JAR file using Gradle (i.e., `gradlew shadowJar`). 1. Tag the repo with the version number. e.g. `v0.1` -1. [Create a new release using GitHub](https://help.github.com/articles/creating-releases/). Upload the JAR file you created. +1. [Create a new release using GitHub](https://help.github.com/articles/creating-releases/). Upload the JAR file you + created. diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 1b56bb5d31b..537832d1e5f 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,14 +2,45 @@ layout: page title: Developer Guide --- -* Table of Contents -{:toc} + +## **Table of Contents** + +1. [Acknowledgements](#acknowledgements) +2. [Setting up, getting started](#setting-up-getting-started) +3. [Design](#design) + * [Architecture](#architecture) + * [UI component](#ui-component) + * [Logic component](#logic-component) + * [Model component](#model-component) + * [Storage component](#storage-component) + * [Common classes](#common-classes) +4. [Implementation](#implementation) + * [Filter feature](#filter-feature) + * [FriendFolio Predicates](#friendfolio-predicates) + * [Remark Command](#remark-command) + * [Lend Command](#lend-command) + * [Split Command](#split-command) + * [PayNow](#paynow) + * [Sort Command](#sort-command) + * [Proposed Undo/redo feature](#proposed-undoredo-feature) +5. [Documentation, logging, testing, configuration, dev-ops](#documentation-logging-testing-configuration-dev-ops) +6. [Appendix: Requirements](#appendix-requirements) + * [Product Scope](#product-scope) + * [User Stories](#user-stories) + * [Use Cases](#use-cases) + * [Non-Functional Requirements](#non-functional-requirements) + * [Glossary](#glossary) +7. [Appendix: Planned Enhancements](#appendix-planned-enhancements) +8. [Appendix: Instructions for manual testing](#appendix-instructions-for-manual-testing) -------------------------------------------------------------------------------------------------------------------- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +[//]: # (* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well}) + +* https://github.com/poonchuanan/Python-PayNow-QR-Code-Generator was referred to for the format of PayNow QR codes as + well as the CRC-16 algorithm. -------------------------------------------------------------------------------------------------------------------- @@ -23,7 +54,9 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md).
-:bulb: **Tip:** The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. +:bulb: **Tip:** The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [ +_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create +and edit diagrams.
### Architecture @@ -36,7 +69,11 @@ Given below is a quick overview of main components and how they interact with ea **Main components of the architecture** -**`Main`** (consisting of classes [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. +**`Main`** (consisting of +classes [`Main`](https://github.com/AY2324S2-CS2103T-T16-2/tp/tree/master/src/main/java/seedu/address/Main.java) +and [`MainApp`](https://github.com/AY2324S2-CS2103T-T16-2/tp/tree/master/src/main/java/seedu/address/MainApp.java)) is +in charge of the app launch and shut down. + * At app launch, it initializes the other components in the correct sequence, and connects them up with each other. * At shut down, it shuts down the other components and invokes cleanup methods where necessary. @@ -51,16 +88,21 @@ The bulk of the app's work is done by the following four components: **How the architecture components interact with each other** -The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues +the command `delete 1`. Each of the four main components (also shown in the diagram above), * defines its *API* in an `interface` with the same name as the Component. -* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. +* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding + API `interface` mentioned in the previous point. -For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. +For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using +the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component +through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the +implementation of a component), as illustrated in the (partial) class diagram below. @@ -68,13 +110,25 @@ The sections below give more details of each component. ### UI component -The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) +The UI component has been enhanced with the addition of a new `DisplayCard` element. This element is responsible for +displaying the currently selected contact's detailed information, enhancing the user experience by providing a more +interactive and comprehensive view of contact details. + +The **API** of this component is specified +in [`Ui.java`](https://github.com/AY2324S2-CS2103T-T16-2/tp/tree/master/src/main/java/seedu/address/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. +The UI consists of a `MainWindow` that is made up of parts +e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc, and the newly added `DisplayCard`. +All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities +between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that +are in the `src/main/resources/view` folder. For example, the layout of +the [`MainWindow`](https://github.com/AY2324S2-CS2103T-T16-2/tp/tree/master/src/main/java/seedu/address/ui/MainWindow.java) +is specified +in [`MainWindow.fxml`](https://github.com/AY2324S2-CS2103T-T16-2/tp/tree/master/src/main/resources/view/MainWindow.fxml) The `UI` component, @@ -85,13 +139,14 @@ The `UI` component, ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2324S2-CS2103T-T16-2/tp/tree/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: -The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example. +The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API +call as an example. ![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) @@ -100,10 +155,13 @@ The sequence diagram below illustrates the interactions within the `Logic` compo How the `Logic` component works: -1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. +1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates + a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. +1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which + is executed by the `LogicManager`. 1. The command can communicate with the `Model` when it is executed (e.g. to delete a person).
- Note that although this is shown as a single step in the diagram above (for simplicity), in the code it can take several interactions (between the command object and the `Model`) to achieve. + Note that although this is shown as a single step in the diagram above (for simplicity), in the code it can take + several interactions (between the command object and the `Model`) to achieve. 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command: @@ -111,11 +169,28 @@ Here are the other classes in `Logic` (omitted from the class diagram above) tha How the parsing works: -* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. -* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. + +* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a + placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse + the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as + a `Command` object. +* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` + interface so that they can be treated similarly where possible e.g, during testing. + +Directly executing commands without user input: + +* The application has instances where some function might be performed on a button click instead of a user input. +* In such cases, the flow bypasses the need to parse a user input, and we directly pass a `Command` object into + the `Logic` class to be executed. + +The following sequence diagram illustrates how the components interact with each other when a user clicks on a button to +reset the debt they have with a specific `Person`. + +![Interactions for when a ResetDebtCommand is manually executed](images/ResetDebtSequenceDiagram.png) ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) + +**API** : [`Model.java`](https://github.com/AY2324S2-CS2103T-T16-2/tp/tree/master/src/main/java/seedu/address/model/Model.java) @@ -123,9 +198,15 @@ How the parsing works: The `Model` component, * stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. -* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. -* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) +* stores a separate _sorted_ list of `Person` objects (e.g., results of a sort query) which is then used to construct + the filtered list below +* stores the currently 'selected' `Person` objects (e.g., results of a filter query) as a separate _filtered_ list which + is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to + this list so that the UI automatically updates when the data in the list change. +* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as + a `ReadOnlyUserPref` objects. +* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they + should make sense on their own without depending on other components)
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
@@ -133,17 +214,20 @@ The `Model` component,
- ### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](https://github.com/AY2324S2-CS2103T-T16-2/tp/tree/master/src/main/java/seedu/address/storage/Storage.java) The `Storage` component, -* can save both address book data and user preference data in JSON format, and read them back into corresponding objects. -* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). -* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) + +* can save both address book data and user preference data in JSON format, and read them back into corresponding + objects. +* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only + the functionality of only one is needed). +* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects + that belong to the `Model`) ### Common classes @@ -155,29 +239,282 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. +### Filter feature + +#### Introduction + +FriendFolio can filter contacts by tags, days available (in a week) and by name using FilterCommand. +FilterCommand can also specify whether the filter is any-match or all-match with respect to the keywords using +the `--all` flag (it is by default any-match). + +Note that the `--all` flag is implemented using ArgumentMultimap, and it must be placed at the end of the command, +for example: + +`filter tag friends colleagues --all` + +Any text after `--all` is not parsed, but will produce the same result as the above command instead of throwing an error. + +#### Implementation + +To reduce code duplication, the abstract FilterCommand extracts identical methods of 3 commands +(FilterNameCommand, FilterTagCommand, FilterDayCommand). Each command now inherits `execute` from FilterCommand, +and has their own error message and command specific information. The filter feature is implemented as follows: + +1. The `filter` command syntax is parsed by FilterCommandParser, where the type of filter + (e.g. by tags, days available (in a week) or name) and the `--all` flag are parsed to create the respective + Predicates. +2. The created Predicates are used to instantiate their respective FilterCommands which inherit `execute` + from the abstract FilterCommand class. +3. The common `execute` function modifies the `ObservableList` in the `Model` component to show the newly-filtered list. + +The sequence diagram below shows how the components interact with each other when the user enters the command `filter tag friend`. + + + +#### FriendFolio Predicates + +Predicates in FriendFolio take in both a list of keywords to match, and a boolean to handle the all-match condition. +There are currently 3 predicates: `NameContainsKeyWordPredicate`, `PersonAvailableOnDayPredicate`, `PersonHasTagPredicate`. + +#### Alternatives Considered + +1. Single FilterCommand Class: Initially considered using a single FilterCommand class to handle every specified filter + type (day, name, tag). This approach was discarded because it is less extensible and OOP. +2. FilterCommand as an Interface: This approach was discarded because of near-identical method logic across + FilterNameCommand, FilterTagCommand and FilterDayCommand which meant significant code-duplication. + +#### UML Diagram + +Refer to the below class diagram to visualize the relationships between FilterCommand, commands inheriting from FilterCommand +and predicates. + + + +### Remark Command + +#### Introduction + +The Remark Command allows users to add or remove remarks for a person in the address book. +This feature enhances the app's usability by enabling users to store additional information about a contact that doesn't +fit into the standard fields like name, phone, or email. + +#### Implementation + +The RemarkCommand is implemented in the following steps: + +1. The user inputs a command in the format remark `INDEX r/REMARK`, where `INDEX` is the position of the person in the + current + list, and `REMARK` is the new remark for the person. +2. The AddressBookParser parses the input and creates a new `RemarkCommand` object. + The `RemarkCommand` executes by: + - Retrieving the person to edit from the model based on the index. + - Creating a new Person object with the updated remark and other details unchanged. + - Replacing the old person object in the model with the updated one. +3. The UI is then updated to display the person's details with the new remark. + +This implementation ensures that the app's performance is unaffected by the addition of remarks, as it reuses the +existing infrastructure for modifying person details. + +#### Alternatives Considered + +1. Storing Remarks Separately: Initially considered storing remarks in a separate map with the Person as the key. + This approach was discarded because it complicated the model's state management and increased the risk of data + inconsistency. +2. Extending Person Model: Another option was to extend the Person model to include remarks as a mandatory field. + However, this was not ideal as remarks are optional and should not affect the creation of Person objects. + +#### UML Diagram + +To illustrate the interaction between components for the remark command, a sequence diagram is provided: + + + +### Lend Command + +#### Implementation Overview + +After the `AddressBookParser` identifies that the user's input is calling the `lend` command word, it creates a +`LendCommandParser`. The `LendCommandParser` then parses the user's input and creates a new `LendCommand` +containing one `Index`. The `LendCommand` is then executed by `LogicManager`, which updates the +`MoneyOwed` attribute in `Person`. A `CommandResult` which stores the message of the outcome of lend command is +then returned. Part of the class diagram is shown below. + + + +When the `LendCommand` executes, it checks if the `Index` is valid based on the last displayed list. +If it is valid, the `MoneyOwed` of the target person will be updated with the new amount. +The following activity diagram sums up the workflow of what happens when the user keys in a lend command. + + + +### Split Command + +#### Implementation Overview + +After the `AddressBookParser` identifies that the user's input is calling the `split` command word, it creates a +`SplitCommandParser`. The `SplitCommandParser` then parses the user's input and creates a new `SplitCommand` +containing one `MoneyOwed` object with the amount to split and at least one `Index`. The `SplitCommand` is then +executed by `LogicManager`, which updates the `MoneyOwed` attribute in `Person`. A `CommandResult` which stores +the message of the outcome of split command is then returned. Part of the class diagram is shown below. + + + +The following activity diagram sums up the workflow of what happens when the user keys in a split command. + + + +This implementation considers the user as an active participant in the split. For example, when the user enters two +indexes, the total amount is evenly divided among the user and the two specified individuals. + +#### Alternatives Considered + +1. Implementing unequal splits of money owed among contacts. However, this approach would necessitate manual + calculations and the input of specific values for each contact, thereby undermining the primary advantage of the + split command — to simplify and automate the distribution process. For scenarios requiring specific, manually + determined amounts, users can utilize the `lend` command, which is designed for financial entry for a single contact. + +### PayNow + +PayNow QR codes are basically encoded string, further encoded into a QR code. The string follow a specific format and +can be generated offline. The specifications of the format have been referenced +from [this repo](https://github.com/poonchuanan/Python-PayNow-QR-Code-Generator). + +Basically, the string represents an object (similar to JSON) and it contains "fields" (similar to JSON attributes). In +one of the required fields is a nested object. + +The class diagram is as such: + +![PayNow Code modelling](images/PayNowDiagram.png) + +`PayNowPayload` is the aforementioned representation of an object. One `PayNowPayload` can contain +multiple `PaynowField`s. + +`PayNowCode` is what we encode into the QR code that we can then scan. One of the fields contain +a `MerchantAccountInformation`, which is also a `PayNowPayload` itself (which is the nested object that had been +mentioned above). + +We then call PayNowCode's static method, passing in a phone number and an initial amount (that will be autofilled when +users scan the QR code with their banking application), to generate the QR code. + +### Sort Command + +FriendFolio can sort contacts in 4 different ways: + +1. Name `name` +2. Money Owed `money` +3. Closest upcoming birthday `birthday` +4. Time they were added into FriendFolio (Default) `clear` + +For example: `sort name` + +We use a [`SortedList`](https://docs.oracle.com/javase/8/javafx/api/javafx/collections/transformation/SortedList.html) to facilitate dynamic sorting by allowing the updating of a `Comparator`. This enables users to toggle between various sorting methods seamlessly. + +This `SortedList` is then used in the constructor of a [`FilteredList`](https://docs.oracle.com/javase/8/javafx/api/javafx/collections/transformation/FilteredList.html) which is used in the [implementation of the filter feature](#filter-feature). + +The `FilteredList` is then used by the UI to display the contacts in the specified order and filters because any changes in the ordering of the contacts from the `SortedList` will be propagated to the `FilteredList`, which will then reflect in the GUI. + +For the first 3 sort types, a static `Comparator` is implemented inside the respective classes themselves (e.g. `Name`, `MoneyOwed`, `Birthday`). +When executing the command, the model will call the `updatePersonComparator` inside the `Model` class. A `null` is passed in as the comparator if `sort clear` is executed. + +The _sequence diagram_ below shows how the components interact with each other when the user enters the command `sort name`. + +![Sequence diagram of sort command](images/SortSequenceDiagram.png) + +### Home Page UI + +##### Overview + +`HomeCard` is a new feature in the UI component designed to provide users with a quick overview of crucial information immediately upon application launch. It serves as a dynamic dashboard, displaying the current date and time, the total number of contacts, a summary of financial transactions, and a list of contacts available for the day. + +![Image of Home Page UI](images/Home Card.png) + +##### Implementation + +The `HomeCard` is implemented using JavaFX and is integrated into the main window of the application. It interacts directly with the `Model` component to fetch real-time updates, ensuring that all displayed information reflects the latest data. + +Key features include: + +- **Real-Time Clock**: Utilizing Java's `LocalDateTime` and `Timeline` to update every second. +- **Contact Counter**: Dynamically updates as contacts are added or removed. +- **Financial Overview**: Uses a `BarChart` to graphically display money owed and owing. +- **Availability List**: Shows contacts available today based on their schedules. + +#### Detailed Design + +The layout for `HomeCard` is defined in `HomeCard.fxml`, organized to provide immediate access to its features, which are essential for enhancing user interaction and providing a comprehensive view at a glance. + +![Sequence diagram of Home Page UI](images/HomeCardSequenceDiagram.png) + +### MiniPersonCard component + +#### Overview + +Embedded within the `HomeCard`, the `MiniPersonCard` serves as a compact display module for contacts available on the current day. It offers a quick snapshot of essential contact details, enhancing the `HomeCard` functionality by allowing users to identify key information swiftly. + +![Image of Display Card UI](images/Mini Person Card.png) + +#### Functionality + +Each `MiniPersonCard` represents an individual contact, showcasing brief but pertinent details such as the contact's name and their availability status. This component is utilized in the `ListView` of the `HomeCard` to enumerate all contacts who are available today. + +#### Design + +Like `DisplayCard`, `MiniPersonCard` is built using JavaFX and defined in `MiniPersonCard.fxml`. The design is minimalistic, focusing on displaying only the most relevant information to maintain the streamlined nature of the `HomeCard`'s overview purpose. + +#### Integration into HomeCard + +`MiniPersonCard` integrates seamlessly into the `HomeCard` to provide a dynamic list that updates daily, showing which contacts are available based on their schedules. This integration is crucial for users who need to quickly assess their contacts' availability without navigating away from the home screen. + +### DisplayCard component + +#### Overview + +The `DisplayCard` enhances the UI by providing detailed information about a selected contact. This component is designed to improve user interaction by offering a more comprehensive and interactive view of contact details. + +![Image of Display Card UI](images/Display Card.png) + +#### Functionality + +`DisplayCard` is responsible for displaying extensive details about a contact, such as name, tags, birthday, and other personal details like debt or remarks. It updates in real-time when users select different contacts within the application. + +#### Design + +The implementation of `DisplayCard` relies on JavaFX, and its layout is managed via `DisplayCard.fxml`. It subscribes to updates from the `Model` component to ensure the displayed information is always current, reflecting any changes made to the contact information immediately. + +![Sequence diagram of Display Card UI](images/DisplayCardSequenceDiagram.png) + ### \[Proposed\] Undo/redo feature #### Proposed Implementation -The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: +The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo +history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the +following operations: -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +* `VersionedAddressBook#commit()`— Saves the current address book state in its history. +* `VersionedAddressBook#undo()`— Restores the previous address book state from its history. +* `VersionedAddressBook#redo()`— Restores a previously undone address book state from its history. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` +and `Model#redoAddressBook()` respectively. Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the +initial address book state, and the `currentStatePointer` pointing to that single address book state. ![UndoRedoState0](images/UndoRedoState0.png) -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command +calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes +to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book +state. ![UndoRedoState1](images/UndoRedoState1.png) -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +Step 3. The user executes `add n/David …​` to add a new person. The `add` command also +calls `Model#commitAddressBook()`, causing another modified address book state to be saved into +the `addressBookStateList`. ![UndoRedoState2](images/UndoRedoState2.png) @@ -185,7 +522,9 @@ Step 3. The user executes `add n/David …​` to add a new person. The `add` co -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing +the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` +once to the left, pointing it to the previous address book state, and restores the address book to that state. ![UndoRedoState3](images/UndoRedoState3.png) @@ -206,17 +545,23 @@ Similarly, how an undo operation goes through the `Model` component is shown bel ![UndoSequenceDiagram](images/UndoSequenceDiagram-Model.png) -The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once +to the right, pointing to the previously undone state, and restores the address book to that state.
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.
-Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such +as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. +Thus, the `addressBookStateList` remains unchanged. ![UndoRedoState4](images/UndoRedoState4.png) -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. +Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not +pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be +purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern +desktop applications follow. ![UndoRedoState5](images/UndoRedoState5.png) @@ -229,13 +574,13 @@ The following activity diagram summarizes what happens when a user executes a ne **Aspect: How undo & redo executes:** * **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. + * Pros: Easy to implement. + * Cons: May have performance issues in terms of memory usage. * **Alternative 2:** Individual command knows how to undo/redo by itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. + * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). + * Cons: We must ensure that the implementation of each individual command are correct. _{more aspects and alternatives to be added}_ @@ -262,44 +607,234 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: +NUS students who want to coordinate weekly meetup sessions for meals/activities + * has a need to manage a significant number of contacts * prefer desktop apps over other types * can type fast * prefers typing to mouse interactions * is reasonably comfortable using CLI apps +* would like to know who is available on a particular day of the week +* would like to know how much he owes to/is owed by his contacts -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app - +**Value proposition**: FriendFolio tailors the contact management experience just for students. It streamlines +connections, enhances academic collaborations, and fosters a vibrant community within their reach. FriendFolio elevates +the networking game, making every interaction meaningful. ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | +| Priority | As a …​ | I want to …​ | So that I can…​ | Notes | +|----------|--------------------------------------------|-------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|---------------------------| +| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | | +| `* * *` | user | add a new person | | | +| `* * *` | user | delete a person | remove entries that I no longer need | | +| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | | +| `* * *` | user | view person's information | | | +| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | | +| `* *` | user | store days that my contacts are in school | see who is free to meet up | | +| `* *` | user | store the phone numbers of our contacts | be able to call our contacts | | +| `* *` | user | store address | be able to visit them | | +| `* *` | user | store tags | be able to store miscellaneous information about them | | +| `* *` | user | store email | email them | | +| `* *` | user | store money owed | remember who owes me money | | +| `* *` | forgetful user | store birthdays of my contacts | not forget when their birthdays are | | +| `* *` | user | store uncategorized information under a field | remember other information that may not be captured in the existing list of fields | “newton doesn’t eat beef” | +| `* *` | user | edit person's information | correct mistakes made when I added the contact | | +| `* *` | user with many persons in the address book | sort persons by name | locate a person easily | | +| `*` | user | filter contacts by tags | find contacts of specific categories such as family, friends | | +| `*` | user | update money owed | | | +| `*` | user | sort contacts by money owed | look at who owes me the most money | | +| `*` | user | sort contacts by birthdays | remember to wish the person for his birthday | | +| `*` | user | be able to split bills easily | I can efficiently manage my expenses and accurately track who owes what | | +| `*` | user | filter contacts by days that my contacts are in school | see who is free to meet up more easily | | +| `*` | user | be warned of creating contacts with duplicate phone numbers | to avoid making duplicate contacts | | +| `*` | user | share/export my contacts | back them up or share them with others | | +| `*` | user | store profile pictures of my contacts | quickly identify and remember my contact | | +| `*` | user | see what is the total amount owed to me/i owe to my contacts | start paying up/ asking others to pay stuff for me | | +| `*` | experienced user | use quick keyboard shortcuts to perform all the implemented functions above | use the address book more efficiently | | +| `*` | new user | input contact information into multiple separate input fields (instead of entering one command) | i can avoid making mistakes by not being familiar with the command format | | +| `*` | user | pin starred contacts at the top of the address book | quickly access my favorite contacts | | +| `*` | user | access my search history on the search bar | quickly access recent searches | | +| `*` | user | store incomplete contacts as drafts | return to my incomplete contacts to finish them up after any disruption without losing existing keyed-in information | | +| `*` | user | access my desired contacts via autocomplete in the search bar | efficiently access my contacts in the address book | | +| `*` | experienced user | add multiple contacts with one input | efficiently use the address book | | +| `*` | user | remove starred contacts from the top of the address book | remove contacts i no longer want to pin | | *{More to be added}* ### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified +otherwise) + +___ + +**Use Case: Add Contact** + +***Preconditions:*** User is logged into the system. + +**MSS** + +1. User selects the option to add a new contact. +2. User enters the contact's name, phone number, and any other optional information. +3. System validates the provided information. +4. System adds the new contact to the address book. +5. System displays a confirmation message. + +***Postconditions:*** A new contact is added to the address book. + +***Alternate Flows:*** If the information fails validation, the system notifies the user and requests correct data. + +___ + +**Use Case: Delete Contact** + +***Preconditions:*** User is logged into the system and the address book contains at least one contact. + +**MSS** + +1. User requests a list of contacts and selects one to delete. +2. System requests confirmation for deletion. +3. User confirms. +4. System deletes the selected contact from the address book. +5. System displays a confirmation message. + +***Postconditions:*** The selected contact is removed from the address book. + +***Alternate Flows:*** If the user cancels the deletion, no action is taken. + +___ + +**Use Case: Edit Contact** -**Use case: Delete a person** +***Preconditions:*** User is logged into the system and the address book contains at least one contact. **MSS** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User requests to edit a contact and selects one from the list. +2. System displays the selected contact’s current information. +3. User updates the necessary information. +4. System validates the updated information. +5. System updates the contact details in the address book. +6. System displays a confirmation message. - Use case ends. +***Postconditions:*** The selected contact's information is updated in the address book. + +***Alternate Flows:*** If the updated information fails validation, the system notifies the user and requests correct +data. + +___ + +**Use Case: Store Phone Number** + +***Preconditions:*** User has initiated adding or editing a contact. + +**MSS** + +1. User provides a phone number for the contact. +2. System validates the phone number format. +3. System stores the phone number with the contact’s information. + +***Postconditions:*** The contact's phone number is stored or updated. + +___ + +**Use Case: View Contacts** + +***Preconditions:*** User is logged into the system. + +**MSS** + +1. User selects the option to view contacts. +2. System retrieves and displays all contacts from the address book. + +***Postconditions:*** User views the list of all contacts in the address book. + +___ + +**Use Case: Store Address** + +***Preconditions:*** User has initiated adding or editing a contact. + +**MSS** + +1. User provides an address for the contact. +2. System validates the address format. +3. System stores the address with the contact’s information. + +***Postconditions:*** The contact's address is stored or updated. + +___ + +**Use Case: Store Tags** + +***Preconditions:*** User has initiated adding or editing a contact. + +**MSS** + +1. User provides one or more tags for the contact. +2. System stores the tags with the contact’s information. + +***Postconditions:*** The contact's tags are stored or updated. + +___ + +**Use Case: Store Email** + +***Preconditions:*** User has initiated adding or editing a contact. + +**MSS** + +1. User provides an email address for the contact. +2. System validates the email format. +3. System stores the email with the contact’s information. + +***Postconditions:*** The contact's email address is stored or updated. + +___ + +**Use Case: Store Money Owed** + +***Preconditions:*** User has initiated adding or editing a contact. + +**MSS** + +1. User provides an amount of money owed for the contact. +2. System validates the money format. +3. System stores the money owed information with the contact’s details. + +***Postconditions:*** The contact's money owed information is stored or updated. + +___ + +**Use Case: Store Birthday** + +***Preconditions:*** User has initiated adding or editing a contact. + +**MSS** + +1. User provides a birthday for the contact. +2. System validates the birthday format. +3. System stores the birthday with the contact’s information. + +***Postconditions:*** The contact's birthday is stored or updated. + +___ + +**Use Case: Store Remarks** + +***Preconditions:*** User has initiated adding or editing a contact. + +**MSS** + +1. User provides some remarks for the contact. +2. System stores the remarks with the contact’s information. + +***Postconditions:*** The contact's remarks are stored or updated. + +___ **Extensions** @@ -317,9 +852,11 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli ### Non-Functional Requirements -1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. +2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be + able to accomplish most of the tasks faster using commands than using the mouse. +4. Should remain functional in the event that the user types in an invalid command. *{More to be added}* @@ -330,6 +867,55 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli -------------------------------------------------------------------------------------------------------------------- +## **Appendix: Planned Enhancements** + +1. **Making Phone Number and Email Address Unique** + + The current FriendFolio system uses a contact's name as a unique identifier. We are planning to enhance this by + switching to using contact's phone number and email as unique identifiers. This change will prevent multiple + individuals from sharing the same email or phone number within the system, while allowing the existence of multiple + individuals with the same name. + +2. **Improved responsiveness of GUI for long text** + + We are aware that excessively long text, like long names, addresses, and remarks etc. might not display fully in + a smaller window. While you are able to make the window larger to display more text, we plan to work on + improving the responsiveness of our user interface to handle longer inputs. + + ![Planned Enhancement 2](images/plannedEnhancement2.jpeg) + +3. **Improved responsiveness of GUI for different screen sizes** + + We are aware that some UI components like the display card, overlap with other components when the screen + size is reduced. We plan to enhance the responsiveness of our application to ensure it dynamically adapts and + supports various display sizes seamlessly in the future. + + ![Planned Enhancement 3](images/plannedEnhancement3.jpeg) + +4. **Maintain information of GUI for different screen sizes** + + We are aware that when the screen size is reduced, some information from the contact details card may get cut off. + We plan to address this issue by enhancing the responsiveness of our interface in future updates, ensuring that + all information remains visible and accessible on smaller screens. + + ![Planned Enhancement 4](images/plannedEnhancement4.jpeg) + +5. **Improve messages to user** + + We are aware that some of our error and success messages could be more informative for our users. + For example, the current success message for edit and add command does not display information + on birthday, money owed and days available. We plan to enhance these messages to provide more + specific information, ensuring a better user experience. + +6. **Use of symbols in names** + + Our application currently supports only alphanumeric characters in names and restricts the use of symbols + such as `/`. However, we recognize that in many cultures, names might include components like `s/o` (son of). + To better accommodate these conventions, we are considering expanding our character allowance to include + certain symbols in future enhancements. + +-------------------------------------------------------------------------------------------------------------------- + ## **Appendix: Instructions for manual testing** Given below are instructions to test the app manually. @@ -343,15 +929,16 @@ testers are expected to do more *exploratory* testing. 1. Initial launch - 1. Download the jar file and copy into an empty folder + 1. Download the jar file and copy into an empty folder - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be + optimum. 1. Saving window preferences - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. + 1. Resize the window to an optimum size. Move the window to a different location. Close the window. - 1. Re-launch the app by double-clicking the jar file.
+ 1. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. 1. _{ more test cases …​ }_ @@ -360,23 +947,163 @@ testers are expected to do more *exploratory* testing. 1. Deleting a person while all persons are being shown - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. + 1. Test case: `delete 1`
+ Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + 1. Test case: `delete 0`
+ Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. + 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. 1. _{ more test cases …​ }_ +### Filter + +1. Filter by `tag` while all persons are being shown + + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 2. Test Case: `filter tag friend`
+ Expected: Person list now shows only persons with the tag `friend`. Status message shows the number of persons + listed. + + 3. Test Case: `filter tag FRIEND`
+ Expected: Person list now shows only persons with the tag `friend`. Status message shows the number of persons + listed. + + 4. Test Case: `filter tag %$#`
+ Expected: Person list still shows all persons. Error details shown in the status message. + Status bar remains the same. + +2. Filter by `name` while all persons are being shown + + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 2. Test Case: `filter name john`
+ Expected: Person list now shows only persons whose name contains the string `john` surrounded by whitespace + (i.e. `john tan` would be in the list but `johnny` will not be in the list if both persons exist initially). + Status message shows the number of persons listed. + + 3. Test Case: `filter name JOHN`
+ Expected: Person list now shows only persons whose name contains the string `john` surrounded by whitespace + (i.e. `john tan` would be in the list but `johnny` will not be in the list if both persons exist initially). + Status message shows the number of persons listed. + +3. Filter by `day` for persons available on multiple days while all persons are being shown + + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 2. Test Case: `filter day monday tuesday --all`
+ Expected: Person list now shows only persons who are available on `monday` and `tuesday`. + Status message shows the number of persons listed. + + 3. Test Case: `filter day today tuesday --all`
+ Expected: Person list still shows all persons. Error details shown in the status message. + Status bar remains the same. + +### Sort + +1. Sort by `name` while all persons are being shown + + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 2. Test Case: `sort name`
+ Expected: Person list now shows all persons in FriendFolio with names sorted in + [lexicographical order](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#compareTo-java.lang.String-) + Status message shows that the sort command has been executed. + +2. Sort by `birthday` while a filtered list is shown + + 1. Prerequisites: List all persons using the `list` command then filter with any valid `filter` command. + Multiple persons in the list. + + 2. Test Case: `sort birthday`
+ Expected: Person list now shows the filtered list of persons sorted based on upcoming birthdays, with those + whose next birthday is closest to today appearing first. + Status message shows that the sort command has been executed. + +3. Sort clear while a sorted list is shown + + 1. Prerequisites: List all persons using the `list` command then sort with any valid `sort` command. + Multiple persons in the list. + + 2. Test Case: `sort clear`
+ Expected: Person list now shows all persons in FriendFolio with names sorted in order of when they were added to + FriendFolio. Status message shows that the sort command has been executed. + +### Pay + +1. Paying a person labelled by index while all persons are being shown + + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 2. Test Case: `pay 1`
+ Expected: PayNow QR window appears containing the QR code generated using the persons' phone number. + Status message shows details of the person you are paying. + +2. Paying a person who does not have a Singaporean number + + 1. Prerequisites: List all persons using the `list` command. The first person in the list has an invalid + phone number (not an 8-digit number starting with 8 or 9). + + 2. Test Case: `pay 1`
+ Expected: PayNow QR window does not appear. Error details shown in the status message. + Status bar remains the same. + +### Lending an amount to a person + +1. Lending money to a person when all persons are being shown + + 1. Prerequisites: List all persons using the `list` command. At least one person in the list with first contact + having $0 for money owed. + + 1. Test case: `lend 1 $/50`
+ Expected: First contact in the list now owes you $50 more. + + 1. Test case: `lend x $/50` (where x is larger than list size)
+ Expected: Amount owed of all contacts remain the same, error details show that index is invalid. + + 1. Test case: `lend 1 $/100001`
+ Expected: Amount owed of first contact remains the same, error details shown in status message. + +### Splitting an amount between user and a group + +1. Splitting an amount between user and a group when all persons are being shown + + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list with first and second + contact having $0 for money owed. + + 1. Test case: `split 1 2 $/30`
+ Expected: First and second contact in the list now owe you $10 more. + + 1. Test case: `split 1 2 $/-30`
+ Expected: Amount owed of first and second contacts remain the same, error details shown in status message. + + 1. Test case: `split 1 2 $/100001`
+ Expected: Amount owed of first and second contacts remain the same, error details shown in status message. + +### Adding remarks to a person + +1. Adding remark to a person when all persons are being shown + + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 1. Test case: `remark 1 r/He likes to swim`
+ Expected: First contact now has remark `He likes to swim`. + + 1. Test case: `remark 1 r/`
+ Expected: First contact now has empty remark. + + 1. Test case: `remark x r/She likes to jog` (where x is larger than list size)
+ Expected: Remark of all contacts remain the same, error details shown in status message. + ### Saving data 1. Dealing with missing/corrupted data files - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ 1. _{ more test cases …​ }_ diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index e913ddab952..c505181e406 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -22,14 +22,12 @@ GEM http_parser.rb (~> 0) ethon (0.16.0) ffi (>= 1.15.0) - eventmachine (1.2.7) eventmachine (1.2.7-x64-mingw32) execjs (2.8.1) faraday (2.7.5) faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-net_http (3.0.2) - ffi (1.15.5) ffi (1.15.5-x64-mingw32) forwardable-extended (2.6.0) gemoji (3.0.1) @@ -207,14 +205,12 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) mercenary (0.3.6) - mini_portile2 (2.8.5) minima (2.5.1) jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) minitest (5.19.0) - nokogiri (1.16.2) - mini_portile2 (~> 2.8.2) + nokogiri (1.16.2-x64-mingw32) racc (~> 1.4) octokit (4.25.1) faraday (>= 1, < 3) @@ -249,9 +245,9 @@ GEM concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext - unf_ext (0.0.8.2) unf_ext (0.0.8.2-x64-mingw32) unicode-display_width (1.8.0) + wdm (0.1.1) webrick (1.8.1) PLATFORMS @@ -261,7 +257,8 @@ PLATFORMS DEPENDENCIES github-pages jekyll + wdm (~> 0.1.0) webrick BUNDLED WITH - 2.1.4 + 2.2.33 diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 7abd1984218..aefaabd10ca 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,45 +1,350 @@ --- layout: page -title: User Guide +title: Using FriendFolio --- -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. +***Welcome to FriendFolio!*** + +Congratulations on joining the FriendFolio community! We're thrilled to have you on board. FriendFolio isn't just your +ordinary address book; it's your ultimate companion for staying organized, managing finances between friends, and +syncing up with your buddies' school schedules effortlessly. + +This user guide is designed to help you navigate every feature of FriendFolio with ease, ensuring you make the most +out of your experience. So sit back, relax, and let's dive into the exciting world of FriendFolio! + +Happy organizing, + +The FriendFolio Dev Team + + + +
+ +***A Quick Overview*** + +FriendFolio is your ultimate companion for simplifying student life! Whether you're **managing your contacts**, +**splitting bills with friends**, or **syncing up with your buddies' schedules**, FriendFolio has got you covered. +It seamlessly combines desktop CLI (Command Line Interface) functionality with intuitive GUI (Graphical User Interface) +elements. So dive in and let FriendFolio revolutionize the way you navigate your social and financial interactions! + +
+ +### Table of Contents + +1. [Introduction](#1-introduction) + - 1.1 [What is FriendFolio?](#11-what-is-friendfolio) + - 1.1 [Why This User Guide Matters](#12-why-this-user-guide-matters) + - 1.2 [Who This User Guide Is For](#13-who-this-user-guide-is-for) +3. [How To Use This Guide](#2-how-to-use-this-guide) + - 2.1 [Navigating This Guide](#21-navigating-this-guide) + - 2.2 [Special Icons](#22-special-icons) +4. [Getting Started](#3-getting-started) + - 3.1 [Installation](#31-installation) + - 3.2 [Starting Up](#32-starting-up) +5. [User Interface Overview](#4-user-interface-overview) + - 4.1 [Dashboard](#41-dashboard) + - 4.2 [Command Box](#42-command-box) + - 4.3 [Contact List](#43-contact-list) +6. [Command Overview](#5-command-overview) + - 5.1 [Parameter Prefixes](#51-parameter-prefixes) + - 5.2 [Data Modification Commands](#52-data-modification-commands) + - [`Add` Command](#adding-a-person-add) + - [`Edit` Command](#editing-a-person-edit) + - [`Delete` Command](#deleting-a-person-delete) + - [`Clear` Command](#clearing-all-entries-clear) + - [`Remark` Command](#adding-or-editing-a-remark-remark) + - 5.3 [Financial Transactions Commands](#53-financial-transactions-commands) + - [`Lend` Command](#lending-an-amount-lend) + - [`Split` Command](#splitting-an-amount-owed-split) + - [`Pay` Command](#generating-payment-qr-code-pay) + - 5.4 [Data Organization Commands](#54-data-organization-commands) + - [`List` Command](#listing-all-persons-list) + - [`Filter` Command](#filtering-based-on-selected-types-filter) + - [`Sort` Command](#sorting-contacts-sort) + - 5.5 [Utility Commands](#55-utility-commands) + - [`Help` Command](#viewing-help-help) + - [`Exit` Command](#exiting-the-program-exit) +7. [Handling Data](#6-handling-data) + - 6.1 [Saving Data Files](#61-saving-the-data) + - 6.2 [Editing Data Files](#62-editing-the-data-file) +8. [FAQs](#7-faq) +9. [Coming Soon](#8-coming-soon-in-v20) + - 8.1 [Unique Phone Numbers and Emails](#81-unique-phone-numbers-and-emails) + - 8.2 [Improved Responsiveness of GUI](#82-improved-responsiveness-of-gui) +10. [Known Issues](#9-known-issues) +11. [Command Summary](#10-command-summary) + +
+ +## 1. Introduction + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 1.1. What is FriendFolio? + +Welcome to FriendFolio, your new digital buddy that's all about simplifying your social life! Imagine having a personal +assistant dedicated to keeping your contacts, social plans, and shared expenses neatly organized - that's FriendFolio +for you! + +FriendFolio isn't just another contact app. It’s a vibrant hub where you can: + +- **Store and Update Contact Information:** Keep your friends close, but their contact details closer! FriendFolio + allows you to effortlessly manage your contacts' information, making sure you're just a click away from connecting. + +- **Split Bills Without the Spills:** Whether you’re dining out or sharing a gift, FriendFolio’s bill-splitting feature + ensures that everyone chips in fairly, so you can focus on the fun, not the finances. + +- **Sync Schedules Like a Pro:** Check who's available and when, so you can plan gatherings or group studies without + the back-and-forth. FriendFolio keeps everyone's schedules in check, ensuring you never miss a beat. + +- **Track Debts with Ease:** Owing and being owed has never been clearer. FriendFolio keeps a neat record of all + financial exchanges among friends, making those awkward money conversations a thing of the past. + +- **Navigate with Nimble Commands:** With its intuitive Command Line Interface (CLI), power users can zip through tasks + with quick commands. + +- **Personalize with Remarks:** Add a personal touch to your contacts with custom remarks, making each entry as unique + as the friend it represents. + +FriendFolio is more than just an app; it's a way to enhance connections, streamline social logistics, and make every +interaction with your friends a little easier and a lot more enjoyable. So go ahead, dive in, and see how FriendFolio can transform your everyday social chaos into organized harmony! + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 1.2. Why This User Guide Matters -* Table of Contents -{:toc} +While FriendFolio is designed to be intuitive and user-friendly, taking a few moments to familiarize yourself with this +guide will significantly enhance your overall experience. Here's why: --------------------------------------------------------------------------------------------------------------------- +- **Unlock Hidden Features**: Uncover useful FriendFolio features that go beyond your everyday address book app + and leverage FriendFolio to its full potential. -## Quick start +- **Streamline Your Experience**: Find useful tips to streamline your FriendFolio experience and navigate the app + effortlessly, saving time and frustration. -1. Ensure you have Java `11` or above installed in your Computer. +- **Maximize Efficiency**: Gain valuable insights and best practices to ensure FriendFolio + maximizes efficiency in your social interactions. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +In essence, this user guide isn't just a manual – it's your key to unlocking the full potential of FriendFolio and +revolutionizing the way you connect with friends. So don't overlook its importance; dive in, explore, and elevate your +FriendFolio experience today! -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 1.3. Who This User Guide is For + +FriendFolio is designed with the needs of college students and young professionals in mind. It is ideal for those who +manage their academic and social lives digitally, dealing with tasks like bill splitting, schedule syncing, and contact +management through a desktop interface. + +#### Assumptions + +We assume that users of this guide: + +- Are familiar with basic computer operations such as clicking, typing, and navigating through software interfaces. +- Have a general understanding of how to install applications and open files on their computers. +- Do not need detailed explanations of common tech terminology but can benefit from a glossary of application-specific + terms which is provided at the end of this guide. + +This guide will help you navigate FriendFolio from basic setup to advanced features, ensuring that you make the most +out of your experience! -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +## 2. How to Use This Guide -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try: +Navigating our user guide efficiently is key to leveraging FriendFolio's full potential quickly! This section will help +you understand how to best use this guide and interpret the icons and formatting used throughout. - * `list` : Lists all contacts. +
+[:arrow_heading_up:](#table-of-contents) +
+
- * `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. +### 2.1. Navigating This Guide - * `delete 3` : Deletes the 3rd contact shown in the current list. +This user guide is structured to help you find information quickly and easily. Here’s how you can navigate through the +guide: - * `clear` : Deletes all contacts. +- **Table of Contents:** At the beginning of the guide, the [Table of Contents](#table-of-contents) provides a + clickable index of the major sections and sub-sections. Use this as your starting point to jump directly to the parts + of the guide that are most relevant to your needs! - * `exit` : Exits the app. +- **Section Headers:** Each major section and sub-section of the guide is clearly labeled with descriptive headers. + As you scroll through the user guide, these headers will help you quickly identify the content of each + section with ease. -1. Refer to the [Features](#features) below for details of each command. +- **Return to Table of Contents Button:** At the bottom right of each section, you'll find a + [:arrow_heading_up:](#table-of-contents) icon. Clicking this takes you straight back to the + [Table of Contents](#table-of-contents), allowing for quick navigation without scrolling. --------------------------------------------------------------------------------------------------------------------- +
+[:arrow_heading_up:](#table-of-contents) +
+
-## Features +### 2.2. Special Icons + +To enhance your understanding and highlight key information, we use three types of special icons throughout this guide: + +
+:bulb: **Tip**: This light bulb icon is used to draw attention to helpful hints and shortcuts. Tips are designed to +improve your use of FriendFolio by providing additional context or simpler methods to perform tasks. +
+ +
+:information_source: **Information**: Whenever you see this information icon, it indicates supplementary details that +provide deeper insights or background information. This could include explanations of complex features, definitions of +terms, or extended usage guidelines. +
+ +
+:warning: **Warning**: The warning icon highlights important actions or steps that require your careful attention to +ensure correct usage of FriendFolio. These warnings help you avoid common pitfalls that could disrupt your workflow. +
+ +
+:exclamation: **Caution**: This icon is used to signal critical information regarding potential risks such as data loss +or configuration issues that could impact your experience. It is crucial to follow these guidelines to prevent data +integrity problems. +
+ +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +## 3. Getting Started + +Welcome to the beginning of your FriendFolio journey! This section walks you through the initial setup steps, from +checking your system's compatibility to launching the application. Follow these straightforward instructions to start +managing your contacts and schedules with ease in no time. + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 3.1. Installation + +1. **Check Your Java Version:** Make sure you have Java `11` or above installed on your computer. You may download Java 11 [here](https://www.oracle.com/java/technologies/downloads/#java11). +2. **Download FriendFolio:** Head over to [this link](https://github.com/AY2324S2-CS2103T-T16-2/tp/releases) and grab the latest `friendfolio.jar` file. + + ![Jar](images/JarFile.png) + +3. **Set Up Your Home Folder:** Copy the downloaded file to the folder you want to use as your _home folder_ for FriendFolio. + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 3.2. Starting Up + +1. **Run the application:** + 1. Open a command terminal (e.g. _Terminal_ on MacOS or _Powershell_ on Windows) + 2. Navigate to the folder containing `friendfolio.jar` using the `cd` command. (e.g. if your `friendfolio.jar` is in `C:/Downloads`, run `cd C:/Downloads`) + 3. Use the command `java -jar friendfolio.jar` to launch the application. + 4. A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data. + ![Ui](images/Ui.png) +2. **Start exploring:** Type commands into the command box and hit Enter to execute them. For example, try typing `help` to open the help window. Here are a few other commands you can try out: + * `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. + * `clear` : Deletes all contacts. + * `exit` : Exits the app. +3. **Command Overview:** Need more details on each command? Check out the [Command Overview](#5-command-overview) below. + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +## 4. User Interface Overview + +When you launch FriendFolio, you will be greeted with some key information on the dashboard. Let's take a tour of what you'll find when you launch the app. + +![Breakdown of Ui](images/UiBreakdown.png) + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 4.1. Dashboard + +Get ready for a quick glance at your day! Here's what you'll find on the dashboard: + +![Dashboard](images/Dashboard.png) + +* **Current Time:** Stay on track with the current time displayed right here, so you don't have to look away. +* **Contact Count:** See how many buddies you've got in your network because the more, the merrier! +* **Finances Graph:** Keep tabs on who owes you and who you owe, all in one neat graph. No more guesswork on who needs a gentle reminder about that borrowed cash. +* **Availability Status:** Know at a glance who's free today, perfect for planning catch-ups or tackling group projects together. + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 4.2. Command Box + +Right at the top of the screen, you'll find the trusty command box, your gateway to managing FriendFolio. +Just type in your commands here, hit Enter, and watch as the results appear right above the box. It's that simple! + +![Command Result](images/CommandResult.png) + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 4.3. Contact List + +On the left is where you can see your contacts. Each contact card is a snapshot of essential details, offering a +peek at your friend's availability and how your balances stand. + +Want to know more? Simply click on a contact card to +reveal in-depth information, including: +![Ui of contact information displayed](images/DisplayCard.png) + +- **Tags:** Quick labels for categorizing your contacts, such as 'commando' for your gym buddies or 'study' for project + group members. +- **Days Available:** A glance at when your friend is free, like 'TUE' for Tuesdays, so you can plan get-togethers or + study sessions with ease. +- **Contact Details:** Dive deeper with a click and see phone numbers, addresses, email, birthday information, and even + the balance of money owed. +- **Remark:** Personal notes you've added, like 'great presenter' or unique traits, to remember the little things that + make each friendship special. + +And when you need to return to the main dashboard, a simple tap of the `'Esc'` key whisks you back. FriendFolio's +Contact List is your personal directory, making social and financial coordination a breeze. + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +## 5. Command Overview + +
+**:warning: Using the PDF version of this guide?** + +If you're _copying and pasting_ commands that **span multiple lines** from the PDF, be aware that space characters surrounding line breaks may be omitted when pasted into the application. Keep an eye out for any missing spaces to ensure your commands work smoothly! +
@@ -49,150 +354,505 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo 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`. + e.g. `n/NAME [t/TAG]` can be used with or without a tag, like `n/John Doe t/friend` or simply `n/John Doe`. + +* Items with `…` after them can be used multiple times.
+ e.g. `[t/TAG]…` can be completely omitted, inlcuded once as `t/friend`, or multiple times like `t/friend t/family`.
+ e.g. `INDICES…` represent a parameter that has to be used at least once because of the absence of square brackets. -* Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. +* Items that start with `--` are flags that alter the command's default behavior.
+ Any redundant text after the flag will be ignored.
+ e.g. `--all` * 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. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
+* Redundant text/parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. -* If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application.
-### Viewing help : `help` +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 5.1. Parameter Prefixes + +Navigating FriendFolio is simple once you master the parameter prefixes! These shortcuts help you communicate +effectively with the system by specifying the data needed for each command. + +Below is a table of the all the prefixes you'll use. Whether you’re adding new contacts or updating existing ones, +these will ensure smooth interactions with FriendFolio. + +| Prefix | Parameter Description | +|--------|-----------------------| +| `n/` | Name of the person | +| `p/` | Phone number | +| `e/` | Email address | +| `a/` | Physical address | +| `b/` | Birthday (dd/mm/yyyy) | +| `$/` | Money owed or owing | +| `t/` | Tags for categorization (multiple allowed) | +| `d/` | Days available (multiple allowed) | +| `r/` | Remark or note associated with the contact | + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 5.2. Data Modification Commands -Shows a message explaning how to access the help page. +These commands are your go-to tools for tailoring your address book to fit your needs. Whether you're adding new +friends, updating details for your current connections, or tidying up your contacts list, these features have you +covered. -![help message](images/helpMessage.png) +
+[:arrow_heading_up:](#table-of-contents) +
+
-Format: `help` +#### **Adding a person: `add`** +Ready to expand your address book? Let's add someone new! -### Adding a person: `add` +Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [b/BIRTHDAY] [$/MONEY_OWED] [t/TAG]… [d/DAY]…​` -Adds a person to the address book. +
+:information_source: **Names in FriendFolio** -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +* Names are unique in FriendFolio, therefore people with the same name must be differentiated either with extra + characters or otherwise. For example, if "John Tan" exists in your contacts: + * E.g: `add n/John Tan p/98765432 e/johnT@example.com a/John street, block 123, #01-01` fails. + * E.g: `add n/John Tan from SoC p/98765432 e/johnT@example.com a/John street, block 123, #01-01` succeeds. +* Note that duplicate name detection is **case-sensitive**, therefore: + * E.g: `add n/john tan p/98765432 e/johnT@example.com a/John street, block 123, #01-01` also succeeds. + +
+ +
+**:information_source: Attribute constraints:** + +* Names are **alphanumeric** and can contain spaces. +* Phone numbers have to be **numeric** and at least 3 digits long. +* Addresses have to start with a **non-whitespace** character. +* Birthdays follow the following format: `dd/mm/yyyy`. +* Money owed ranges from -100,000 to 100,000 and has to be a **numeric** input with at most **2** decimal places. This means that maximum total amount you can owe or a person owes you is $100,000. +* Emails are a little tricky, but in short, a valid email is in the format `local-part@domain`, where: + * The local-part contains only **alphanumeric** characters and any number of these special characters: `+_.-`, but may not start or end with them. + * The domain consists of one or more labels separated by `.`, e.g. `gmail.com`. + * Don't worry, as long as the email you enter follows the conventional email format, you're good to go! +* Tags have to be **alphanumeric**. + +
:bulb: **Tip:** -A person can have any number of tags (including 0) +A person can have any number of tags or days available (including 0)
Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` - -### Listing all persons : `list` -Shows a list of all persons in the address book. +* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 b/15/02/1999` +* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* `add n/Plain Jane t/friend e/plainjane@example.com a/Newgate Prison p/2345678 b/01/01/2001 d/monday $/100` -Format: `list` +
+[:arrow_heading_up:](#table-of-contents) +
+
-### Editing a person : `edit` +#### **Editing a person: `edit`** -Edits an existing person in the address book. +Time for some updates! Let's tweak an existing entry in your address book. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]… [b/BIRTHDAY] [$/MONEY_OWED] [d/DAY]…​` -* 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, …​ +* Edits the person at the specified `INDEX`. The index refers to the index number shown in the contact list on the left.
+ Make sure it's a **positive integer** (e.g. 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. +* Existing values will be overwritten by the input values. +* When editing tags, the existing tags of the person will be removed i.e. adding of tags is not cumulative. -Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +
+:question: Need a refresher on the constraints for some of these fields? Refer to the attribute constraints [here](#attribute-constraints)! +
-### Locating persons by name: `find` +
+:bulb: **Tip:** You can remove all the person’s tags by typing `t/` without specifying any tags after it. +
-Finds persons whose names contain any of the given keywords. +
+:bulb: **Tip:** You can use the `filter` command and then the `edit` command. The index you provide will be based on the filter result. This applies to any command that takes in an index. +
-Format: `find KEYWORD [MORE_KEYWORDS]` +Examples: -* 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` +* `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. -Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +
+[:arrow_heading_up:](#table-of-contents) +
+
-### Deleting a person : `delete` +#### **Deleting a person: `delete`** -Deletes the specified person from the address book. +Time to trim the roster! Let's remove someone from your address book. Format: `delete INDEX` * Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index **must be a positive integer** 1, 2, 3, …​ +* The index refers to the index number shown in the contact list on the left. +* Make sure it's a **positive integer** (e.g. 1, 2, 3, …) Examples: + * `list` followed by `delete 2` deletes the 2nd person in the address book. -* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. +* `filter name Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. + +
+[:arrow_heading_up:](#table-of-contents) +
+
-### Clearing all entries : `clear` +#### **Clearing all entries: `clear`** -Clears all entries from the address book. +Ready to start fresh? Use the `clear` command to wipe out all entries from your address book. Format: `clear` -### Exiting the program : `exit` +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +#### **Adding or Editing a Remark: `remark`** + +Let's add a personal touch! Use the `remark` command to add or edit remarks for your contacts. + +Format: `remark INDEX r/[REMARK]` + +Parameters: + +- `INDEX`: The index number shown in the contact list. Must be a positive integer. +- `r/[REMARK]`: The remark to add or edit for the person. Leave this blank to remove any existing remarks. -Exits the program. +Example: + +- `remark 1 r/Likes to swim.` This command replaces the remark of the first person in the contact list with "Likes to swim". + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 5.3. Financial Transactions Commands + +Navigate your shared finances with ease using these commands. Whether you're sorting out who owes what after a group +dinner or planning expenses for a project, these tools make money matters straightforward and stress-free. + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +#### **Lending an amount: `lend`** + +Ready to lend a hand (or some cash)? Use the lend command to manage money transactions with your contacts. + +Format: `lend INDEX $/MONEY_OWED` + +* Use **positive** `MONEY_OWED` to lend money to the person, and **negative** `MONEY_OWED` to borrow from them. +* `MONEY_OWED` ranges from -100,000 to 100,000. +* The index refers to the number shown in the contact list. Ensure it's a **positive integer** (e.g., 1, 2, 3). + +Examples: + +* If the **first two** people in the contact list owe you $3 now, + * `lend 1 $/2` → first person owes you $5 now + * `lend 2 $/-1.50` → second person owes you $0.50 now + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +#### **Splitting an amount owed: `split`** + +Time to divide and conquer! Use the split command to evenly distribute a sum of money owed among yourself and a group of people. + +Format: `split INDICES… $/MONEY_OWED` + +* MONEY_OWED be a positive number at most 100,000 and have **at most 2 decimal places**. +* The amount will be evenly distributed among you and the group of people with index mentioned and the split amount will be added on to their current amount of money owed. +* The amount the person owes you after splitting cannot exceed 100,000. +* The amount after splitting should be at least 0.01. +* There must be **at least 1** index. +* The index refers to the index number shown in the contact list. + +Examples: + +* `split 1 2 $/6.60` evenly divides $6.60 among you and two other people, adding $2.20 to the amount owed by the first and second people in your contact list. + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +#### **Generating payment QR code: `pay`** + +Ready to make payments a breeze? Use the pay command to generate a QR code for quick transactions. + +Format: `pay INDEX` + +* The person at that index should have a valid **Singaporean number** that is **registered with PayNow**. +* The index refers to the index number shown in the contact list. + +Examples: + +* `pay 3` will generate a PayNow QR code for the third person in the contact list. + +##### QR Code Window + + + +* After the QR code is displayed, you can scan it with your local banking application to pay the user. If you owe them money, that amount will be set as the default payment, but you can adjust the amount within your banking application. +* If you owe the person money, you can click on the **Clear Debt** button to reset your money owed to $0 and close this window. +* Otherwise, click on the **Close Window** button to exit. + +
+:warning: **Potential errors:** + +* Invalid index. +* The person at the index does not have a valid Singaporean number. +* The person's number is not registered to PayNow. + +
+ +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 5.4. Data Organization Commands + +These commands help you organize and retrieve contact information quickly, ensuring you always find what you need when +you need it. + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +#### **Listing all persons: `list`** + +Want to see everyone who is in your address book? Just type `list` to get a full rundown! + +Format: `list` + +
+**:bulb: Tip:** You can use the `list` command after a [`filter`](#filtering-based-on-selected-types-filter) command to get back the original list of contacts. +
+ +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +#### **Filtering based on selected types: `filter`** + +Let's narrow down your search! Use the filter command to refine your contact list based on specific criteria. +You can choose to filter by day available, by name or by tags, and specify if the returned contacts should match any +or all of the keywords specified using the `--all` optional flag at the end of the command. + +Format: + +1. `filter tag TAG_NAME… [--all]` OR +2. `filter name PERSON_NAME… [--all]` OR +3. `filter day DAY… [--all]` + +* **At least one** of the types (`tag`, `name`, or `day`) needs to be used. +* If multiple `TAG_NAME`, `PERSON_NAME` or `DAY` is used, the default result + returned will be all matching contacts to any of the keywords. +* Adding the --all flag ensures only contacts matching all the keywords are shown. Any text after the flag will be ignored! + +Examples: + +* `filter tag friend` returns contacts that has the tag "friend" attached to them. +* `filter day wednesday friday` returns contacts available on Wednesday, Friday, or both. +* `filter day monday tuesday --all` returns all contacts available on **both** Monday and Tuesday. + +
:bulb: **Tip:** +Use the `list` command after a `filter` command to reset any filters and display all contacts! This will not affect the current order of contacts, if you have used the [`sort`](#sorting-contacts-sort) command. +
+ +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +#### **Sorting contacts: `sort`** + +Time to tidy things up! Use the sort command to organize your contacts in a way that suits you best. + +Format: `sort SORT_METHOD` + +`SORT_METHOD` should be one of the following keywords: + +1. `name`: Sorts contacts alphabetically by name. +2. `birthday`: Arranges contacts based on upcoming birthdays, with those closest to today appearing first. +3. `money`: Prioritizes contacts based on the amount owed, with those owed the most money appearing first, followed by those who owe you the most. +4. `clear`: Resets the sorting method to the default, listing contacts in the order they were added to FriendFolio. + +
:bulb: **Tip:** +Feel free to use the [`filter`](#filtering-based-on-selected-types-filter) command together with this command to filter our your contacts and show them in whichever order you please! +
+ +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 5.5. Utility Commands + +Essential tools that enhance your interaction with FriendFolio, providing support and easy navigation. + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +#### **Viewing help: `help`** + +Need a hand? Just type `help` to access the help page and get the guidance you need! + +![help message](images/helpMessage.png) + +Format: `help` + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +#### **Exiting the program: `exit`** + +Ready to sign off? Just use the `exit` command to close the program. Format: `exit` -### Saving the data +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +## 6. Handling Data + +Efficiently managing and safeguarding your data is crucial to getting the most out of FriendFolio. This section guides +you through the essential processes of saving and editing your data securely. -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +
+[:arrow_heading_up:](#table-of-contents) +
+
-### Editing the data file +### 6.1. Saving the data -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +Your FriendFolio data is automatically saved to the hard disk after any command that alters the data. No manual saving required! -
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
-Furthermore, certain edits can cause the AddressBook to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly. +
+[:arrow_heading_up:](#table-of-contents)
+
-### Archiving data files `[coming in v2.0]` +### 6.2. Editing the data file -_Details coming soon ..._ +FriendFolio automatically saves your data as a JSON file located at `[JAR file location]/data/addressbook.json`. Advanced users can directly edit this data file if needed. --------------------------------------------------------------------------------------------------------------------- +
:exclamation: **Caution:** +If your changes to the data file makes its format invalid, FriendFolio will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
+Furthermore, certain edits can cause the FriendFolio to behave in unexpected ways (e.g., if a value entered is outside the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly. +
-## FAQ +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +## 7. FAQ **Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains +the data of your previous FriendFolio home folder. --------------------------------------------------------------------------------------------------------------------- +
+[:arrow_heading_up:](#table-of-contents) +
+
-## Known issues +## 8. Coming Soon in v2.0 -1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. +Gear up for an even smoother and more personalized FriendFolio experience with our upcoming version 2.0. We're +fine-tuning the details to make sure your FriendFolio journey is as unique as your friendships. --------------------------------------------------------------------------------------------------------------------- +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 8.1. Unique Phone Numbers and Emails + +Affects `add`, `edit` + +FriendFolio is looking to make the person's phone number and email the unique identifiers in the future. +This change aims to prevent multiple individuals from sharing the same email or phone number within the system +while allowing multiple individuals with the same name to exist. + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +### 8.2. Improved responsiveness of GUI -## Command summary +We are aware that excessively long text, like long names, addresses, and remarks etc. might not display fully in a smaller window. While you are able to make the window larger to display more text, we seek your patience while we work on improving the responsiveness of our user interface to handle longer inputs. -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +## 9. Known issues + +1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only + the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the + application before running the application again. + +
+[:arrow_heading_up:](#table-of-contents) +
+
+ +## 10. Command summary + +| Action | Format, Examples | +|------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [b/BIRTHDAY] [$/MONEY_OWED] [t/TAG]… [d/DAY]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague b/01/01/2001 d/monday $/100` | +| **Clear** | `clear` | +| **Delete** | `delete INDEX`
e.g., `delete 3` | +| **Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [$/MONEY_OWED] [b/BIRTHDAY] [t/TAG]… [d/DAY]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` | +| **Exit** | `exit` | +| **Filter** | `filter TYPE KEYWORD [--all]`
e.g., `filter day wednesday friday --all`, `filter tag family` | +| **Help** | `help` | +| **Lend** | `lend INDEX $/MONEY_OWED`
e.g., `lend 1 $/2.50`, `lend 2 $/-1.65` | +| **List** | `list` | +| **Pay** | `pay INDEX`
e.g., `pay 3` | +| **Sort** | `sort SORT_METHOD`
e.g., `sort birthday` | +| **Split** | `split INDICES… $/MONEY_OWED`
e.g., `split 1 2 $/20.10` | +| **Remark** | `remark INDEX r/[REMARK]`
e.g., `remark 1 r/Likes to swim.` | + +
+[:arrow_heading_up:](#table-of-contents) +
diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..7a0846cf5c1 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "FriendFolio" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2324S2-CS2103T-T16-2/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..a3ec9bff00c 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -14,7 +14,6 @@ dl, dd, ol, ul, figure { } - /** * Basic styling */ @@ -24,9 +23,9 @@ body { background-color: $background-color; -webkit-text-size-adjust: 100%; -webkit-font-feature-settings: "kern" 1; - -moz-font-feature-settings: "kern" 1; - -o-font-feature-settings: "kern" 1; - font-feature-settings: "kern" 1; + -moz-font-feature-settings: "kern" 1; + -o-font-feature-settings: "kern" 1; + font-feature-settings: "kern" 1; font-kerning: normal; display: flex; min-height: 100vh; @@ -35,7 +34,6 @@ body { } - /** * Set `margin-bottom` to maintain vertical rhythm */ @@ -59,7 +57,6 @@ main { } - /** * Images */ @@ -69,7 +66,6 @@ img { } - /** * Figures */ @@ -82,7 +78,6 @@ figcaption { } - /** * Lists */ @@ -98,7 +93,6 @@ li { } - /** * Headings */ @@ -107,7 +101,6 @@ h1, h2, h3, h4, h5, h6 { } - /** * Links */ @@ -154,7 +147,6 @@ blockquote { } - /** * Code formatting */ @@ -193,7 +185,6 @@ pre { } - /** * Wrapper */ @@ -213,7 +204,6 @@ pre { } - /** * Clearfix */ @@ -224,7 +214,6 @@ pre { } - /** * Icons */ @@ -247,18 +236,22 @@ table { color: $table-text-color; border-collapse: collapse; border: 1px solid $table-border-color; + tr { &:nth-child(even) { background-color: $table-zebra-color; } } + th, td { padding: ($spacing-unit / 3) ($spacing-unit / 2); } + th { background-color: $table-header-bg-color; border: 1px solid $table-header-border; } + td { border: 1px solid $table-border-color; } @@ -267,7 +260,7 @@ table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; + -ms-overflow-style: -ms-autohiding-scrollbar; } } @@ -288,7 +281,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "FriendFolio"; font-size: 32px; } } diff --git a/docs/_sass/minima/_layout.scss b/docs/_sass/minima/_layout.scss index ca99f981701..131d0b31407 100644 --- a/docs/_sass/minima/_layout.scss +++ b/docs/_sass/minima/_layout.scss @@ -24,7 +24,7 @@ &, &:visited { - color: $brand-color-dark; + color: white; } } @@ -74,7 +74,7 @@ } .page-link { - color: $text-color; + color: white; line-height: $base-line-height; display: block; padding: 5px 10px; @@ -86,6 +86,7 @@ margin-left: 20px; } + @media screen and (min-width: $on-medium) { position: static; float: right; @@ -176,8 +177,8 @@ .post-content { margin-bottom: $spacing-unit; - h1, h2, h3 { margin-top: $spacing-unit * 2 } - h4, h5, h6 { margin-top: $spacing-unit } + h1, h2, h3 { margin-top: $spacing-unit } + h4, h5, h6 { margin-top: $spacing-unit / 2 } h2 { @include relative-font-size(1.75); diff --git a/docs/_sass/minima/custom-styles.scss b/docs/_sass/minima/custom-styles.scss index 56b5d56b430..98929570c6f 100644 --- a/docs/_sass/minima/custom-styles.scss +++ b/docs/_sass/minima/custom-styles.scss @@ -1,7 +1,7 @@ // Placeholder to allow defining custom styles that override everything else. // (Use `_sass/minima/custom-variables.scss` to override variable defaults) -h2, h3, h4, h5, h6 { - color: #e46c0a; +h1, h2, h3, h4, h5, h6 { + color: #0a7873; } // Bootstrap style alerts diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss index b5ec6976efa..4e5fe7abf11 100644 --- a/docs/assets/css/style.scss +++ b/docs/assets/css/style.scss @@ -10,3 +10,7 @@ height: 21px; width: 21px } + +.site-header { + background: linear-gradient(90deg, rgba(17,64,83,1) 0%, rgba(9,121,114,1) 43%, rgba(0,212,255,1) 100%); +} diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..75dfe918bd5 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -18,4 +18,8 @@ Person *--> Name Person *--> Phone Person *--> Email Person *--> Address +Person *--> Remark +Person *--> Birthday +Person *--> MoneyOwed +Person --> "*" Day @enduml diff --git a/docs/diagrams/DisplayCardSequenceDiagram.puml b/docs/diagrams/DisplayCardSequenceDiagram.puml new file mode 100644 index 00000000000..33807ce9ac5 --- /dev/null +++ b/docs/diagrams/DisplayCardSequenceDiagram.puml @@ -0,0 +1,70 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box "Initialization" UI_COLOR_T1 +participant ":PersonListPanel" as PersonListPanel UI_COLOR +participant "displayCard:DisplayCard" as DisplayCard UI_COLOR +participant ":Person" as Person MODEL_COLOR +end box + +create DisplayCard +PersonListPanel -> DisplayCard : DisplayCard(person) +activate DisplayCard + +DisplayCard -> DisplayCard : setUpLabels(person) +activate DisplayCard + + +DisplayCard -> Person : getName() +activate Person +Person --> DisplayCard +deactivate Person +DisplayCard -> Person : getPhone() +activate Person +Person --> DisplayCard +deactivate Person +DisplayCard -> Person : getAddress() +activate Person +Person --> DisplayCard +deactivate Person +DisplayCard -> Person : getEmail() +activate Person +Person --> DisplayCard +deactivate Person +DisplayCard -> Person : getRemark() +activate Person +Person --> DisplayCard +deactivate Person +DisplayCard -> Person : getTags() +activate Person +Person --> DisplayCard +deactivate Person +DisplayCard -> Person : getDaysAvailable() +activate Person +Person -->DisplayCard +deactivate Person + +DisplayCard -> Person : getBirthday() +activate Person +Person --> DisplayCard +deactivate Person +DisplayCard -> Person : getMoneyOwed() +activate Person +Person --> DisplayCard +deactivate Person + +DisplayCard --> DisplayCard +deactivate DisplayCard + +DisplayCard -> DisplayCard : setUpIcons() +activate DisplayCard +deactivate DisplayCard + +DisplayCard -> DisplayCard : playAnimation() +activate DisplayCard +deactivate DisplayCard + +DisplayCard --> PersonListPanel +deactivate DisplayCard +@enduml diff --git a/docs/diagrams/FilterClassDiagram.puml b/docs/diagrams/FilterClassDiagram.puml new file mode 100644 index 00000000000..077377c46dd --- /dev/null +++ b/docs/diagrams/FilterClassDiagram.puml @@ -0,0 +1,29 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +left to right direction + +Class "{abstract}\nCommand" as Command +Class "{abstract}\nFilter" as Filter +Class "<>\nPredicate" as Predicate + +skinparam groupInheritance 2 + +Command <|-- Filter + +Filter <|-- FilterNameCommand +Filter <|-- FilterTagCommand +Filter <|-- FilterDayCommand +Filter --> "1" Predicate + +PersonHasTagPredicate ..|> Predicate +NameContainsKeywordsPredicate ..|> Predicate +PersonAvailableOnDayPredicate ..|> Predicate + +FilterNameCommand o--> "1" NameContainsKeywordsPredicate +FilterTagCommand o--> "1" PersonHasTagPredicate +FilterDayCommand o--> "1" PersonAvailableOnDayPredicate +@enduml diff --git a/docs/diagrams/FilterTagSequenceDiagram.puml b/docs/diagrams/FilterTagSequenceDiagram.puml new file mode 100644 index 00000000000..7b3775ef51a --- /dev/null +++ b/docs/diagrams/FilterTagSequenceDiagram.puml @@ -0,0 +1,78 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant "parser:FilterCommandParser" as FilterCommandParser LOGIC_COLOR +participant "predicate:PersonHasTagPredicate" as PersonHasTagPredicate LOGIC_COLOR +participant "command:FilterTagCommand" as FilterTagCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("filter tag friend") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("filter tag friend") +activate AddressBookParser + +create FilterCommandParser +AddressBookParser -> FilterCommandParser +activate FilterCommandParser + +FilterCommandParser --> AddressBookParser +deactivate FilterCommandParser + +AddressBookParser -> FilterCommandParser : parse("filter tag friend") +activate FilterCommandParser + +create PersonHasTagPredicate +FilterCommandParser -> PersonHasTagPredicate +activate PersonHasTagPredicate + +PersonHasTagPredicate --> FilterCommandParser +deactivate PersonHasTagPredicate + +create FilterTagCommand +FilterCommandParser -> FilterTagCommand +activate FilterTagCommand + +FilterTagCommand --> FilterCommandParser +deactivate FilterTagCommand + +FilterCommandParser --> AddressBookParser : command +deactivate FilterCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +FilterCommandParser -[hidden]-> AddressBookParser +destroy FilterCommandParser + +AddressBookParser --> LogicManager : command +deactivate AddressBookParser + +LogicManager -> FilterTagCommand : execute() +activate FilterTagCommand + +FilterTagCommand -> Model : updateFilteredPersonList(predicate) +activate Model + +Model --> FilterTagCommand +deactivate Model + +create CommandResult +FilterTagCommand -> CommandResult +activate CommandResult + +CommandResult --> FilterTagCommand +deactivate CommandResult + +FilterTagCommand --> LogicManager : r +deactivate FilterTagCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/HomeCardSequenceDiagram.puml b/docs/diagrams/HomeCardSequenceDiagram.puml new file mode 100644 index 00000000000..61bd9a6539b --- /dev/null +++ b/docs/diagrams/HomeCardSequenceDiagram.puml @@ -0,0 +1,93 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box "Initialization" UI_COLOR_T1 +participant ":UiManager" as UiManager UI_COLOR +participant "mainWindow:MainWindow" as MainWindow UI_COLOR +participant "logic:Logic" as Logic LOGIC_COLOR +participant "personListPanel:PersonListPanel" as PersonListPanel UI_COLOR +participant "homeCard:HomeCard" as HomeCard UI_COLOR +participant AvailableTodayCell UI_COLOR +participant MiniPersonCard UI_COLOR +end box + +create Logic +UiManager -> Logic : Logic() +activate Logic +Logic --> UiManager +deactivate Logic + +create MainWindow +UiManager -> MainWindow : MainWindow(primaryStage, logic) +activate MainWindow + +create PersonListPanel +MainWindow -> PersonListPanel : PersonListPanel(logic.getFilteredPersonList(), logic.getSortedPersonList()) +activate PersonListPanel + +create HomeCard +PersonListPanel -> HomeCard : HomeCard(logic.getSortedPersonList()) +activate HomeCard + +box "HomeCard Initialization" UI_COLOR_T1 +HomeCard -> HomeCard : setUpTimeline() +activate HomeCard +deactivate HomeCard + +HomeCard -> HomeCard : setUpMoneyChart() +activate HomeCard +deactivate HomeCard + +HomeCard -> HomeCard : setUpAvailableTodayList() +activate HomeCard +HomeCard -> HomeCard : setCellFactory(lambda) +activate HomeCard +create AvailableTodayCell +loop for each person in availableTodayList +HomeCard -> AvailableTodayCell : AvailableTodayCell() +activate AvailableTodayCell +AvailableTodayCell --> HomeCard +deactivate AvailableTodayCell +end +deactivate HomeCard + +loop for each person in availableTodayList + HomeCard -> AvailableTodayCell : updateItem(person, false) + activate AvailableTodayCell + create MiniPersonCard + AvailableTodayCell -> MiniPersonCard : MiniPersonCard(person) + activate MiniPersonCard + MiniPersonCard --> AvailableTodayCell + deactivate MiniPersonCard + AvailableTodayCell --> HomeCard + deactivate AvailableTodayCell +end + +HomeCard -> HomeCard : playAnimation() +activate HomeCard +deactivate HomeCard +end box + +deactivate HomeCard + +HomeCard --> PersonListPanel +deactivate HomeCard + +PersonListPanel --> MainWindow +deactivate PersonListPanel + +MainWindow --> UiManager +deactivate MainWindow + +@enduml + + + + + + + + + + diff --git a/docs/diagrams/LendActivityDiagram.puml b/docs/diagrams/LendActivityDiagram.puml new file mode 100644 index 00000000000..2240bf2ba54 --- /dev/null +++ b/docs/diagrams/LendActivityDiagram.puml @@ -0,0 +1,25 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +: User enters a lend command; + +: LendCommandParser parses the user input and checks if input is valid; + +switch () +case([the LendCommand is valid]) + : Creates a LendCommand which is executed by the LogicManager; + switch() + case([Index in range]) + : Updates target Person in FilterPersonList to have the updated amount owed; + : Updates the FilterPersonList in the Model; + case([else]) + : Throws an error; + endswitch +case([else]) + : Throws an error; +endswitch +stop +@enduml diff --git a/docs/diagrams/LendClassDiagram.puml b/docs/diagrams/LendClassDiagram.puml new file mode 100644 index 00000000000..d12bc1ce05b --- /dev/null +++ b/docs/diagrams/LendClassDiagram.puml @@ -0,0 +1,28 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Package Lend as LendPackage<>{ + +Class LogicManager +Class AddressBookParser +Class LendCommandParser +Class LendCommand +Class Index +Class CommandResult + + +LogicManager -down-> "1" LendCommand : executes > +LogicManager -right-> "1" AddressBookParser +AddressBookParser ..> LendCommandParser : creates > +LendCommand -right-> "1" MoneyOwed +LendCommandParser ..> LendCommand : creates > +LendCommand -right-> "1" Index +LendCommand ..down> CommandResult : generates > +MoneyOwed -[hidden]down-> Index + +} + +@enduml diff --git a/docs/diagrams/MiniPersonCardSequenceDiagram.puml b/docs/diagrams/MiniPersonCardSequenceDiagram.puml new file mode 100644 index 00000000000..f20f4b4d066 --- /dev/null +++ b/docs/diagrams/MiniPersonCardSequenceDiagram.puml @@ -0,0 +1,24 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box "MiniPersonCard" UI_COLOR_T1 +participant "HomeCard" as HomeCard UI_COLOR +participant "MiniPersonCard" as MiniPersonCard UI_COLOR +participant "Model" as Model MODEL_COLOR +participant "PersonList" as PersonList MODEL_COLOR +end box + +User -> HomeCard : openApp() +activate HomeCard +HomeCard -> Model : getAvailableTodayList() +Model --> HomeCard : todayList +loop for each person in todayList + HomeCard -> MiniPersonCard : new(person) + activate MiniPersonCard + MiniPersonCard -> MiniPersonCard : displayBasicInfo() + MiniPersonCard --> HomeCard : displayCompleted() + deactivate MiniPersonCard +end +deactivate HomeCard +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..c3aa24f9410 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -19,6 +19,10 @@ Class Email Class Name Class Phone Class Tag +Class Remark +Class Birthday +Class MoneyOwed +Class Day Class I #FFFFFF } @@ -42,6 +46,10 @@ Person *--> Phone Person *--> Email Person *--> Address Person *--> "*" Tag +Person *--> Remark +Person *--> Birthday +Person *--> MoneyOwed +Person --> "~* daysAvailable" Day Person -[hidden]up--> I UniquePersonList -[hidden]right-> I diff --git a/docs/diagrams/RemarkCommandSequenceDiagram.puml b/docs/diagrams/RemarkCommandSequenceDiagram.puml new file mode 100644 index 00000000000..3797c689949 --- /dev/null +++ b/docs/diagrams/RemarkCommandSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":RemarkCommandParser" as RemarkCommandParser LOGIC_COLOR +participant "r:RemarkCommand" as RemarkCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("remark 1 r/Likes swimming") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("remark 1 r/Likes swimming") +activate AddressBookParser + +create RemarkCommandParser +AddressBookParser -> RemarkCommandParser +activate RemarkCommandParser + +RemarkCommandParser --> AddressBookParser +deactivate RemarkCommandParser + +AddressBookParser -> RemarkCommandParser : parse("1 r/Likes swimming") +activate RemarkCommandParser + +create RemarkCommand +RemarkCommandParser -> RemarkCommand +activate RemarkCommand + +RemarkCommand --> RemarkCommandParser : +deactivate RemarkCommand + +RemarkCommandParser --> AddressBookParser : r +deactivate RemarkCommandParser +'RemarkCommandParser -[hidden]-> AddressBookParser +destroy RemarkCommandParser + +AddressBookParser --> LogicManager : r +deactivate AddressBookParser + +LogicManager -> RemarkCommand : execute(m) +activate RemarkCommand + +RemarkCommand -> Model : setPerson(personToEdit, editedPerson) +activate Model + +Model --> RemarkCommand +deactivate Model + +create CommandResult +RemarkCommand -> CommandResult +activate CommandResult + +CommandResult --> RemarkCommand +deactivate CommandResult + +RemarkCommand --> LogicManager : r +deactivate RemarkCommand + +[<-- LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ResetDebtSequenceDiagram.puml b/docs/diagrams/ResetDebtSequenceDiagram.puml new file mode 100644 index 00000000000..e5e0f36935c --- /dev/null +++ b/docs/diagrams/ResetDebtSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Ui UI_COLOR_T1 +participant ":PaymentWindow" as PaymentWindow UI_COLOR +participant ":MainWindow" as MainWindow UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant "command:ResetDebtCommand" as ResetDebtCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> PaymentWindow : button click +activate PaymentWindow + +PaymentWindow -> MainWindow : onResetDebt() +activate MainWindow + +create ResetDebtCommand +MainWindow -> ResetDebtCommand +activate ResetDebtCommand + +ResetDebtCommand -> MainWindow +deactivate ResetDebtCommand + +MainWindow -> MainWindow : execute(command) +activate MainWindow + +MainWindow -> LogicManager : execute(command) +activate LogicManager + +LogicManager -> ResetDebtCommand : execute(command) +activate ResetDebtCommand + +ResetDebtCommand -> Model : resetDebt(person) +activate Model + +Model --> ResetDebtCommand +deactivate Model + +create CommandResult +ResetDebtCommand -> CommandResult +activate CommandResult + +CommandResult --> ResetDebtCommand +deactivate CommandResult + +ResetDebtCommand --> LogicManager : r +deactivate ResetDebtCommand + +LogicManager --> MainWindow : r +deactivate LogicManager + +MainWindow --> MainWindow +deactivate MainWindow + +MainWindow --> PaymentWindow +deactivate MainWindow + +[<--PaymentWindow +deactivate PaymentWindow +@enduml diff --git a/docs/diagrams/SortSequenceDiagram.puml b/docs/diagrams/SortSequenceDiagram.puml new file mode 100644 index 00000000000..ec61c38a169 --- /dev/null +++ b/docs/diagrams/SortSequenceDiagram.puml @@ -0,0 +1,70 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant "parser:SortCommandParser" as SortCommandParser LOGIC_COLOR +participant "command:SortCommand" as SortCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("sort name") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("sort name") +activate AddressBookParser + +create SortCommandParser +AddressBookParser -> SortCommandParser +activate SortCommandParser + +SortCommandParser --> AddressBookParser +deactivate SortCommandParser + +AddressBookParser -> SortCommandParser : parse("sort name") +activate SortCommandParser + +create SortCommand +SortCommandParser -> SortCommand +activate SortCommand + +SortCommand --> SortCommandParser +deactivate SortCommand + +SortCommandParser --> AddressBookParser : command +deactivate SortCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +SortCommandParser -[hidden]-> AddressBookParser +destroy SortCommandParser + +AddressBookParser --> LogicManager : command +deactivate AddressBookParser + +LogicManager -> SortCommand : execute() +activate SortCommand + +SortCommand -> Model : updatePersonComparator(comparator) +activate Model + +Model --> SortCommand +deactivate Model + +create CommandResult +SortCommand -> CommandResult +activate CommandResult + +CommandResult --> SortCommand +deactivate CommandResult + +SortCommand --> LogicManager : r +deactivate SortCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/SplitActivityDiagram.puml b/docs/diagrams/SplitActivityDiagram.puml new file mode 100644 index 00000000000..bb9b5997722 --- /dev/null +++ b/docs/diagrams/SplitActivityDiagram.puml @@ -0,0 +1,27 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +: User enters a split command; + +: SplitCommandParser parses the user input and checks if input is valid; + + +switch () +case([the split command is valid]) + : Creates a SplitCommand which is executed by the LogicManager; + : Splits the total amount among the number of people; + switch() + case([Index in range]) + : Updates Person in FilterPersonList to have the updated amount owed; + : Updates the FilterPersonList in the Model; + case([else]) + : Throws an error; + endswitch +case([else]) + : Throws an error; +endswitch +stop +@enduml diff --git a/docs/diagrams/SplitClassDiagram.puml b/docs/diagrams/SplitClassDiagram.puml new file mode 100644 index 00000000000..ae892783840 --- /dev/null +++ b/docs/diagrams/SplitClassDiagram.puml @@ -0,0 +1,28 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Package Split as SplitPackage<>{ + +Class LogicManager +Class AddressBookParser +Class SplitCommandParser +Class SplitCommand +Class Index +Class CommandResult + + +LogicManager -down-> "1" SplitCommand : executes > +LogicManager -right-> "1" AddressBookParser +AddressBookParser ..> SplitCommandParser : creates > +SplitCommand -right-> "1" MoneyOwed +SplitCommandParser ..> SplitCommand : creates > +SplitCommand -right-> "1..*" Index +SplitCommand ..down> CommandResult : generates > +MoneyOwed -[hidden]down-> Index + +} + +@enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..a711d593cd2 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -37,6 +37,9 @@ MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow PersonListPanel -down-> "*" PersonCard +PersonListPanel -down-> "0..1" DisplayCard +PersonListPanel -down-> "0..1" HomeCard +HomeCard -down-> "*" MiniPersonCard MainWindow -left-|> UiPart @@ -46,8 +49,12 @@ PersonListPanel --|> UiPart PersonCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +DisplayCard --|> UiPart PersonCard ..> Model +DisplayCard ..> Model +HomeCard ..> Model +MiniPersonCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic diff --git a/docs/diagrams/paynow/PayNow.puml b/docs/diagrams/paynow/PayNow.puml new file mode 100644 index 00000000000..b89827493cf --- /dev/null +++ b/docs/diagrams/paynow/PayNow.puml @@ -0,0 +1,24 @@ +@startuml +!include ../style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR_T1 +skinparam classBackgroundColor MODEL_COLOR_T1 + +Class "{abstract}\nPayNowPayload" as PayNowPayload +Class PayNowCode { + +{static}generateQrCode(String, float) +} +Class MerchantAccountInformation +Class PayNowField { + -int id + -Object value +} +show PayNowField fields +show PayNowCode methods + +PayNowPayload *--> "fields" PayNowField +MerchantAccountInformation --|> PayNowPayload +PayNowCode --|> PayNowPayload +PayNowCode .. MerchantAccountInformation + +@enduml diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..6f41494e7f3 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/CommandResult.png b/docs/images/CommandResult.png new file mode 100644 index 00000000000..589ba97802a Binary files /dev/null and b/docs/images/CommandResult.png differ diff --git a/docs/images/Dashboard.png b/docs/images/Dashboard.png new file mode 100644 index 00000000000..b8706bbffd6 Binary files /dev/null and b/docs/images/Dashboard.png differ diff --git a/docs/images/Display Card.png b/docs/images/Display Card.png new file mode 100644 index 00000000000..163d559d6d7 Binary files /dev/null and b/docs/images/Display Card.png differ diff --git a/docs/images/DisplayCard.png b/docs/images/DisplayCard.png new file mode 100644 index 00000000000..b1c147614de Binary files /dev/null and b/docs/images/DisplayCard.png differ diff --git a/docs/images/DisplayCardSequenceDiagram.png b/docs/images/DisplayCardSequenceDiagram.png new file mode 100644 index 00000000000..0f1b5c23c60 Binary files /dev/null and b/docs/images/DisplayCardSequenceDiagram.png differ diff --git a/docs/images/FilterClassDiagram.png b/docs/images/FilterClassDiagram.png new file mode 100644 index 00000000000..a160edde0d2 Binary files /dev/null and b/docs/images/FilterClassDiagram.png differ diff --git a/docs/images/FilterTagSequenceDiagram.png b/docs/images/FilterTagSequenceDiagram.png new file mode 100644 index 00000000000..ffa7150a934 Binary files /dev/null and b/docs/images/FilterTagSequenceDiagram.png differ diff --git a/docs/images/Home Card.png b/docs/images/Home Card.png new file mode 100644 index 00000000000..3fee3edfce1 Binary files /dev/null and b/docs/images/Home Card.png differ diff --git a/docs/images/HomeCardSequenceDiagram.png b/docs/images/HomeCardSequenceDiagram.png new file mode 100644 index 00000000000..70eccc26d14 Binary files /dev/null and b/docs/images/HomeCardSequenceDiagram.png differ diff --git a/docs/images/JarFile.png b/docs/images/JarFile.png new file mode 100644 index 00000000000..a3d1b310ea2 Binary files /dev/null and b/docs/images/JarFile.png differ diff --git a/docs/images/LendActivityDiagram.png b/docs/images/LendActivityDiagram.png new file mode 100644 index 00000000000..021c0002c3b Binary files /dev/null and b/docs/images/LendActivityDiagram.png differ diff --git a/docs/images/LendClassDiagram.png b/docs/images/LendClassDiagram.png new file mode 100644 index 00000000000..7885c8e6555 Binary files /dev/null and b/docs/images/LendClassDiagram.png differ diff --git a/docs/images/Mini Person Card.png b/docs/images/Mini Person Card.png new file mode 100644 index 00000000000..efa9927387b Binary files /dev/null and b/docs/images/Mini Person Card.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..393413c94ef 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/PayNowDiagram.png b/docs/images/PayNowDiagram.png new file mode 100644 index 00000000000..1f00fb78183 Binary files /dev/null and b/docs/images/PayNowDiagram.png differ diff --git a/docs/images/PayNowWindow.png b/docs/images/PayNowWindow.png new file mode 100644 index 00000000000..1d1acd2d23b Binary files /dev/null and b/docs/images/PayNowWindow.png differ diff --git a/docs/images/RemarkCommandSequenceDiagram.png b/docs/images/RemarkCommandSequenceDiagram.png new file mode 100644 index 00000000000..e4bc342e68e Binary files /dev/null and b/docs/images/RemarkCommandSequenceDiagram.png differ diff --git a/docs/images/ResetDebtSequenceDiagram.png b/docs/images/ResetDebtSequenceDiagram.png new file mode 100644 index 00000000000..edea2ac1f71 Binary files /dev/null and b/docs/images/ResetDebtSequenceDiagram.png differ diff --git a/docs/images/Signature.png b/docs/images/Signature.png new file mode 100644 index 00000000000..177f42d6a46 Binary files /dev/null and b/docs/images/Signature.png differ diff --git a/docs/images/SortSequenceDiagram.png b/docs/images/SortSequenceDiagram.png new file mode 100644 index 00000000000..56bc51bcb3b Binary files /dev/null and b/docs/images/SortSequenceDiagram.png differ diff --git a/docs/images/SplitActivityDiagram.png b/docs/images/SplitActivityDiagram.png new file mode 100644 index 00000000000..5103ba11525 Binary files /dev/null and b/docs/images/SplitActivityDiagram.png differ diff --git a/docs/images/SplitClassDiagram.png b/docs/images/SplitClassDiagram.png new file mode 100644 index 00000000000..c1defac16f1 Binary files /dev/null and b/docs/images/SplitClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..a6272fd3f17 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiBreakdown.png b/docs/images/UiBreakdown.png new file mode 100644 index 00000000000..4fc5fbb7eb3 Binary files /dev/null and b/docs/images/UiBreakdown.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 11f06d68671..040f31f23a5 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UiContactClicked.png b/docs/images/UiContactClicked.png new file mode 100644 index 00000000000..a6569a05959 Binary files /dev/null and b/docs/images/UiContactClicked.png differ diff --git a/docs/images/alvinnzz.png b/docs/images/alvinnzz.png new file mode 100644 index 00000000000..56122341994 Binary files /dev/null and b/docs/images/alvinnzz.png differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png index 235da1c273e..620c15c8062 100644 Binary files a/docs/images/findAlexDavidResult.png and b/docs/images/findAlexDavidResult.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..905f7e61274 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/jerryo3.png b/docs/images/jerryo3.png new file mode 100644 index 00000000000..a51422b6bb9 Binary files /dev/null and b/docs/images/jerryo3.png differ diff --git a/docs/images/newtonkoh.png b/docs/images/newtonkoh.png new file mode 100644 index 00000000000..cc905750158 Binary files /dev/null and b/docs/images/newtonkoh.png differ diff --git a/docs/images/plannedEnhancement2.jpeg b/docs/images/plannedEnhancement2.jpeg new file mode 100644 index 00000000000..45e26238ec2 Binary files /dev/null and b/docs/images/plannedEnhancement2.jpeg differ diff --git a/docs/images/plannedEnhancement3.jpeg b/docs/images/plannedEnhancement3.jpeg new file mode 100644 index 00000000000..4dde39a2989 Binary files /dev/null and b/docs/images/plannedEnhancement3.jpeg differ diff --git a/docs/images/plannedEnhancement4.jpeg b/docs/images/plannedEnhancement4.jpeg new file mode 100644 index 00000000000..ef7686b7f41 Binary files /dev/null and b/docs/images/plannedEnhancement4.jpeg differ diff --git a/docs/images/zhekaiii.png b/docs/images/zhekaiii.png new file mode 100644 index 00000000000..f39a35b4450 Binary files /dev/null and b/docs/images/zhekaiii.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..f53661cac69 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,20 @@ --- layout: page -title: AddressBook Level-3 +title: FriendFolio --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![CI Status](https://github.com/AY2324S2-CS2103T-T16-2/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S2-CS2103T-T16-2/tp/actions) +[![codecov](https://codecov.io/gh/AY2324S2-CS2103T-T16-2/tp/branch/master/graph/badge.svg)](https://codecov.io/gh/AY2324S2-CS2103T-T16-2/tp) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). - -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +**FriendFolio is a desktop application for managing your contact details.** While it has a GUI, most of the user +interactions happen using a CLI (Command Line Interface). +* If you are interested in using FriendFolio, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing FriendFolio, the [**Developer Guide**](DeveloperGuide.html) is a good place to + start. **Acknowledgements** -* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) +* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5), [zxing](https://github.com/zxing/zxing/) diff --git a/docs/tutorials/AddRemark.md b/docs/tutorials/AddRemark.md index d98f38982e7..bcc7115de61 100644 --- a/docs/tutorials/AddRemark.md +++ b/docs/tutorials/AddRemark.md @@ -3,22 +3,26 @@ layout: page title: "Tutorial: Adding a command" --- -Let's walk you through the implementation of a new command — `remark`. +Let's walk you through the implementation of a new command —`remark`. -This command allows users of the AddressBook application to add optional remarks to people in their address book and edit it if required. The command should have the following format: +This command allows users of the AddressBook application to add optional remarks to people in their address book and +edit it if required. The command should have the following format: `remark INDEX r/REMARK` (e.g., `remark 2 r/Likes baseball`) We’ll assume that you have already set up the development environment as outlined in the Developer’s Guide. - ## Create a new `remark` command -Looking in the `logic.command` package, you will notice that each existing command have their own class. All the commands inherit from the abstract class `Command` which means that they must override `execute()`. Each `Command` returns an instance of `CommandResult` upon success and `CommandResult#feedbackToUser` is printed to the `ResultDisplay`. +Looking in the `logic.command` package, you will notice that each existing command have their own class. All the +commands inherit from the abstract class `Command` which means that they must override `execute()`. Each `Command` +returns an instance of `CommandResult` upon success and `CommandResult#feedbackToUser` is printed to +the `ResultDisplay`. Let’s start by creating a new `RemarkCommand` class in the `src/main/java/seedu/address/logic/command` directory. -For now, let’s keep `RemarkCommand` as simple as possible and print some output. We accomplish that by returning a `CommandResult` with an accompanying message. +For now, let’s keep `RemarkCommand` as simple as possible and print some output. We accomplish that by returning +a `CommandResult` with an accompanying message. **`RemarkCommand.java`:** @@ -43,9 +47,12 @@ public class RemarkCommand extends Command { ### Hook `RemarkCommand` into the application -Now that we have our `RemarkCommand` ready to be executed, we need to update `AddressBookParser#parseCommand()` to recognize the `remark` keyword. Add the new command to the `switch` block by creating a new `case` that returns a new instance of `RemarkCommand`. +Now that we have our `RemarkCommand` ready to be executed, we need to update `AddressBookParser#parseCommand()` to +recognize the `remark` keyword. Add the new command to the `switch` block by creating a new `case` that returns a new +instance of `RemarkCommand`. -You can refer to the changes in this [diff](https://github.com/se-edu/addressbook-level3/commit/35eb7286f18a029d39cb7a29df8f172a001e4fd8#diff-399c284cb892c20b7c04a69116fcff6ccc0666c5230a1db8e4a9145def8fa4ee). +You can refer to the changes in +this [diff](https://github.com/se-edu/addressbook-level3/commit/35eb7286f18a029d39cb7a29df8f172a001e4fd8#diff-399c284cb892c20b7c04a69116fcff6ccc0666c5230a1db8e4a9145def8fa4ee). ### Run the application @@ -55,7 +62,9 @@ Run `Main#main` and try out your new `RemarkCommand`. If everything went well, y ## Change `RemarkCommand` to throw an exception -While we have successfully printed a message to `ResultDisplay`, the command does not do what it is supposed to do. Let’s change the command to throw a `CommandException` to accurately reflect that our command is still a work in progress. +While we have successfully printed a message to `ResultDisplay`, the command does not do what it is supposed to do. +Let’s change the command to throw a `CommandException` to accurately reflect that our command is still a work in +progress. ![The relationship between RemarkCommand and Command](../images/add-remark/RemarkCommandClass.png) @@ -88,7 +97,9 @@ Let’s change `RemarkCommand` to parse input from the user. ### Make the command accept parameters -We start by modifying the constructor of `RemarkCommand` to accept an `Index` and a `String`. While we are at it, let’s change the error message to echo the values. While this is not a replacement for tests, it is an obvious way to tell if our code is functioning as intended. +We start by modifying the constructor of `RemarkCommand` to accept an `Index` and a `String`. While we are at it, let’s +change the error message to echo the values. While this is not a replacement for tests, it is an obvious way to tell if +our code is functioning as intended. ``` java import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; @@ -134,17 +145,21 @@ public class RemarkCommand extends Command { } ``` -Your code should look something like [this](https://github.com/se-edu/addressbook-level3/commit/dc6d5139d08f6403da0ec624ea32bd79a2ae0cbf#diff-a8e35af8f9c251525063fae36c9852922a7e7195763018eacec60f3a4d87c594) after you are done. +Your code should look something +like [this](https://github.com/se-edu/addressbook-level3/commit/dc6d5139d08f6403da0ec624ea32bd79a2ae0cbf#diff-a8e35af8f9c251525063fae36c9852922a7e7195763018eacec60f3a4d87c594) +after you are done. ### Parse user input Now let’s move on to writing a parser that will extract the index and remark from the input provided by the user. -Create a `RemarkCommandParser` class in the `seedu.address.logic.parser` package. The class must extend the `Parser` interface. +Create a `RemarkCommandParser` class in the `seedu.address.logic.parser` package. The class must extend the `Parser` +interface. ![The relationship between Parser and RemarkCommandParser](../images/add-remark/RemarkCommandParserClass.png) -Thankfully, `ArgumentTokenizer#tokenize()` makes it trivial to parse user input. Let’s take a look at the JavaDoc provided for the function to understand what it does. +Thankfully, `ArgumentTokenizer#tokenize()` makes it trivial to parse user input. Let’s take a look at the JavaDoc +provided for the function to understand what it does. **`ArgumentTokenizer.java`:** @@ -162,7 +177,9 @@ Thankfully, `ArgumentTokenizer#tokenize()` makes it trivial to parse user input. */ ``` -We can tell `ArgumentTokenizer#tokenize()` to look out for our new prefix `r/` and it will return us an instance of `ArgumentMultimap`. Now let’s find out what we need to do in order to obtain the Index and String that we need. Let’s look through `ArgumentMultimap` : +We can tell `ArgumentTokenizer#tokenize()` to look out for our new prefix `r/` and it will return us an instance +of `ArgumentMultimap`. Now let’s find out what we need to do in order to obtain the Index and String that we need. Let’s +look through `ArgumentMultimap` : **`ArgumentMultimap.java`:** @@ -177,7 +194,8 @@ public Optional getValue(Prefix prefix) { } ``` -This appears to be what we need to get a String of the remark. But what about the Index? Let's take a quick peek at existing `Command` that uses an index to see how it is done. +This appears to be what we need to get a String of the remark. But what about the Index? Let's take a quick peek at +existing `Command` that uses an index to see how it is done. **`DeleteCommandParser.java`:** @@ -188,7 +206,8 @@ return new DeleteCommand(index); There appears to be another utility class that obtains an `Index` from the input provided by the user. -Now that we have the know-how to extract the data that we need from the user’s input, we can parse the user command and create a new instance of `RemarkCommand`, as given below. +Now that we have the know-how to extract the data that we need from the user’s input, we can parse the user command and +create a new instance of `RemarkCommand`, as given below. **`RemarkCommandParser.java`:** @@ -223,24 +242,33 @@ If you are stuck, check out the sample ## Add `Remark` to the model -Now that we have all the information that we need, let’s lay the groundwork for propagating the remarks added into the in-memory storage of person data. We achieve that by working with the `Person` model. Each field in a Person is implemented as a separate class (e.g. a `Name` object represents the person’s name). That means we should add a `Remark` class so that we can use a `Remark` object to represent a remark given to a person. +Now that we have all the information that we need, let’s lay the groundwork for propagating the remarks added into the +in-memory storage of person data. We achieve that by working with the `Person` model. Each field in a Person is +implemented as a separate class (e.g. a `Name` object represents the person’s name). That means we should add a `Remark` +class so that we can use a `Remark` object to represent a remark given to a person. ### Add a new `Remark` class -Create a new `Remark` in `seedu.address.model.person`. Since a `Remark` is a field that is similar to `Address`, we can reuse a significant bit of code. +Create a new `Remark` in `seedu.address.model.person`. Since a `Remark` is a field that is similar to `Address`, we can +reuse a significant bit of code. -A copy-paste and search-replace later, you should have something like [this](https://github.com/se-edu/addressbook-level3/commit/4516e099699baa9e2d51801bd26f016d812dedcc#diff-41bb13c581e280c686198251ad6cc337cd5e27032772f06ed9bf7f1440995ece). Note how `Remark` has no constrains and thus does not require input +A copy-paste and search-replace later, you should have something +like [this](https://github.com/se-edu/addressbook-level3/commit/4516e099699baa9e2d51801bd26f016d812dedcc#diff-41bb13c581e280c686198251ad6cc337cd5e27032772f06ed9bf7f1440995ece). +Note how `Remark` has no constrains and thus does not require input validation. ### Make use of `Remark` -Let’s change `RemarkCommand` and `RemarkCommandParser` to use the new `Remark` class instead of plain `String`. These should be relatively simple changes. +Let’s change `RemarkCommand` and `RemarkCommandParser` to use the new `Remark` class instead of plain `String`. These +should be relatively simple changes. ## Add a placeholder element for remark to the UI -Without getting too deep into `fxml`, let’s go on a 5 minute adventure to get some placeholder text to show up for each person. +Without getting too deep into `fxml`, let’s go on a 5 minute adventure to get some placeholder text to show up for each +person. -Simply add the following to [`seedu.address.ui.PersonCard`](https://github.com/se-edu/addressbook-level3/commit/850b78879582f38accb05dd20c245963c65ea599#diff-639834f1e05afe2276a86372adf0fe5f69314642c2d93cfa543d614ce5a76688). +Simply add the following +to [`seedu.address.ui.PersonCard`](https://github.com/se-edu/addressbook-level3/commit/850b78879582f38accb05dd20c245963c65ea599#diff-639834f1e05afe2276a86372adf0fe5f69314642c2d93cfa543d614ce5a76688). **`PersonCard.java`:** @@ -249,10 +277,11 @@ Simply add the following to [`seedu.address.ui.PersonCard`](https://github.com/s private Label remark; ``` +`@FXML` is an annotation that marks a private or protected field and makes it accessible to FXML. It might sound like +Greek to you right now, don’t worry — we will get back to it later. -`@FXML` is an annotation that marks a private or protected field and makes it accessible to FXML. It might sound like Greek to you right now, don’t worry — we will get back to it later. - -Then insert the following into [`main/resources/view/PersonListCard.fxml`](https://github.com/se-edu/addressbook-level3/commit/850b78879582f38accb05dd20c245963c65ea599#diff-d44c4f51c24f6253c277a2bb9bc440b8064d9c15ad7cb7ceda280bca032efce9). +Then insert the following +into [`main/resources/view/PersonListCard.fxml`](https://github.com/se-edu/addressbook-level3/commit/850b78879582f38accb05dd20c245963c65ea599#diff-d44c4f51c24f6253c277a2bb9bc440b8064d9c15ad7cb7ceda280bca032efce9). **`PersonListCard.fxml`:** @@ -270,11 +299,13 @@ Since `PersonCard` displays data from a `Person`, we need to update `Person` to ### Modify `Person` -We change the constructor of `Person` to take a `Remark`. We will also need to define new fields and accessors accordingly to store our new addition. +We change the constructor of `Person` to take a `Remark`. We will also need to define new fields and accessors +accordingly to store our new addition. ### Update other usages of `Person` -Unfortunately, a change to `Person` will cause other commands to break, you will have to modify these commands to use the updated `Person`! +Unfortunately, a change to `Person` will cause other commands to break, you will have to modify these commands to use +the updated `Person`!
@@ -282,18 +313,20 @@ Unfortunately, a change to `Person` will cause other commands to break, you will
-Refer to [this commit](https://github.com/se-edu/addressbook-level3/commit/ce998c37e65b92d35c91d28c7822cd139c2c0a5c) and check that you have got everything in order! - +Refer to [this commit](https://github.com/se-edu/addressbook-level3/commit/ce998c37e65b92d35c91d28c7822cd139c2c0a5c) and +check that you have got everything in order! ## Updating Storage -AddressBook stores data by serializing `JsonAdaptedPerson` into `json` with the help of an external library — Jackson. Let’s update `JsonAdaptedPerson` to work with our new `Person`! +AddressBook stores data by serializing `JsonAdaptedPerson` into `json` with the help of an external library — Jackson. +Let’s update `JsonAdaptedPerson` to work with our new `Person`! While the changes to code may be minimal, the test data will have to be updated as well.
-:exclamation: You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not doing so will cause AddressBook to default to an empty address book! +:exclamation: You must delete AddressBook’s storage file located at `/data/addressbook.json` before running it! Not +doing so will cause AddressBook to default to an empty address book!
@@ -304,7 +337,8 @@ to see what the changes entail. Now that we have finalized the `Person` class and its dependencies, we can now bind the `Remark` field to the UI. -Just add [this one line of code!](https://github.com/se-edu/addressbook-level3/commit/5b98fee11b6b3f5749b6b943c4f3bd3aa049b692) +Just +add [this one line of code!](https://github.com/se-edu/addressbook-level3/commit/5b98fee11b6b3f5749b6b943c4f3bd3aa049b692) **`PersonCard.java`:** @@ -319,11 +353,14 @@ public PersonCard(Person person, int displayedIndex) { ## Putting everything together -After the previous step, we notice a peculiar regression — we went from displaying something to nothing at all. However, this is expected behavior as we are yet to update the `RemarkCommand` to make use of the code we've been adding in the last few steps. +After the previous step, we notice a peculiar regression — we went from displaying something to nothing at all. However, +this is expected behavior as we are yet to update the `RemarkCommand` to make use of the code we've been adding in the +last few steps. ### Update `RemarkCommand` and `RemarkCommandParser` -In this last step, we modify `RemarkCommand#execute()` to change the `Remark` of a `Person`. Since all fields in a `Person` are immutable, we create a new instance of a `Person` with the values that we want and +In this last step, we modify `RemarkCommand#execute()` to change the `Remark` of a `Person`. Since all fields in +a `Person` are immutable, we create a new instance of a `Person` with the values that we want and save it with `Model#setPerson()`. **`RemarkCommand.java`:** @@ -367,11 +404,16 @@ save it with `Model#setPerson()`. ## Writing tests -Tests are crucial to ensuring that bugs don’t slip into the codebase unnoticed. This is especially true for large code bases where a change might lead to unintended behavior. +Tests are crucial to ensuring that bugs don’t slip into the codebase unnoticed. This is especially true for large code +bases where a change might lead to unintended behavior. Let’s verify the correctness of our code by writing some tests! -Of course you can simply add the test cases manually, like you've been doing all along this tutorial. The result would be like the test cases in [here](https://github.com/se-edu/addressbook-level3/commit/fac8f3fd855d55831ca0cc73313b5943d49d4d6e#diff-ff58f7c10338b34f76645df49b71ecb2bafaf7611b20e7ff59ebc98475538a01). Alternatively, you can get the help of IntelliJ to generate the skeletons of the test cases, as explained in the next section. +Of course you can simply add the test cases manually, like you've been doing all along this tutorial. The result would +be like the test cases +in [here](https://github.com/se-edu/addressbook-level3/commit/fac8f3fd855d55831ca0cc73313b5943d49d4d6e#diff-ff58f7c10338b34f76645df49b71ecb2bafaf7611b20e7ff59ebc98475538a01). +Alternatively, you can get the help of IntelliJ to generate the skeletons of the test cases, as explained in the next +section. ### Automatically generating tests @@ -380,7 +422,8 @@ The goal is to write effective and efficient tests to ensure that `RemarkCommand The convention for test names is `methodName_testScenario_expectedResult`. An example would be `execute_filteredList_success`. -Let’s create a test for `RemarkCommand#execute()` to test that adding a remark works. On `IntelliJ IDEA` you can bring up the context menu and choose to `Go To` \> `Test` or use the appropriate keyboard shortcut. +Let’s create a test for `RemarkCommand#execute()` to test that adding a remark works. On `IntelliJ IDEA` you can bring +up the context menu and choose to `Go To` \> `Test` or use the appropriate keyboard shortcut. ![Using the context menu to jump to tests](../images/add-remark/ContextMenu.png) @@ -390,9 +433,12 @@ Then, create a test for the `execute` method. Following convention, let’s change the name of the generated method to `execute_addRemarkUnfilteredList_success`. -Let’s use the utility functions provided in `CommandTestUtil`. The functions ensure that commands produce the expected `CommandResult` and output the correct message. In this case, `CommandTestUtil#assertCommandSuccess` is the best fit as we are testing that a `RemarkCommand` will successfully add a `Remark`. +Let’s use the utility functions provided in `CommandTestUtil`. The functions ensure that commands produce the +expected `CommandResult` and output the correct message. In this case, `CommandTestUtil#assertCommandSuccess` is the +best fit as we are testing that a `RemarkCommand` will successfully add a `Remark`. -You should end up with a test that looks something like [this](https://github.com/se-edu/addressbook-level3/commit/fac8f3fd855d55831ca0cc73313b5943d49d4d6e#diff-ff58f7c10338b34f76645df49b71ecb2bafaf7611b20e7ff59ebc98475538a01R36-R49). +You should end up with a test that looks something +like [this](https://github.com/se-edu/addressbook-level3/commit/fac8f3fd855d55831ca0cc73313b5943d49d4d6e#diff-ff58f7c10338b34f76645df49b71ecb2bafaf7611b20e7ff59ebc98475538a01R36-R49). ## Conclusion diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..76358b91e2f 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -36,7 +36,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(1, 2, 1, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -65,6 +65,7 @@ public void init() throws Exception { logic = new LogicManager(model, storage); ui = new UiManager(logic); + //Font.loadFont(getClass().getResourceAsStream("/fonts/SF-Pro.ttf"), 12); } /** diff --git a/src/main/java/seedu/address/commons/util/AnimationUtil.java b/src/main/java/seedu/address/commons/util/AnimationUtil.java new file mode 100644 index 00000000000..4581761bbbd --- /dev/null +++ b/src/main/java/seedu/address/commons/util/AnimationUtil.java @@ -0,0 +1,42 @@ +package seedu.address.commons.util; + +import javafx.animation.FadeTransition; +import javafx.animation.TranslateTransition; +import javafx.scene.Node; +import javafx.util.Duration; + +/** + * Utility methods for animations + */ +public class AnimationUtil { + private static final double MOVE_DURATION = 200; + private static final double MOVE_INITIAL = 500; + private static final double MOVE_POP = 20; + private static final double FADE_DURATION = 300; + private static final double FADE_INITIAL = 0; + private static final double FADE_ULTIMATE = 1; + + public static TranslateTransition getBounceBackTransition(Node node) { + double originalPosition = node.getTranslateX(); + TranslateTransition bounceBackTransition = new TranslateTransition(Duration.millis(MOVE_DURATION), node); + bounceBackTransition.setFromX(originalPosition - MOVE_POP); + bounceBackTransition.setToX(originalPosition); + bounceBackTransition.setDelay(Duration.millis(MOVE_DURATION)); + return bounceBackTransition; + } + + public static FadeTransition getFadeInTransition(Node node) { + FadeTransition fadeInTransition = new FadeTransition(Duration.millis(FADE_DURATION), node); + fadeInTransition.setFromValue(FADE_INITIAL); + fadeInTransition.setToValue(FADE_ULTIMATE); + return fadeInTransition; + } + + public static TranslateTransition getMoveTransition(Node node) { + double originalPosition = node.getTranslateX(); + TranslateTransition moveTransition = new TranslateTransition(Duration.millis(MOVE_DURATION), node); + moveTransition.setFromX(originalPosition + MOVE_INITIAL); + moveTransition.setToX(originalPosition - MOVE_POP); + return moveTransition; + } +} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..414fb27f87a 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -4,6 +4,7 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; @@ -16,13 +17,23 @@ public interface Logic { /** * Executes the command and returns the result. + * * @param commandText The command as entered by the user. * @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 ParseException If an error occurs during parsing. */ CommandResult execute(String commandText) throws CommandException, ParseException; + /** + * Directly executes the command and returns the result. + * + * @param command The command to be executed. + * @return the result of the command execution. + * @throws CommandException If an error occurs during command execution. + */ + CommandResult execute(Command command) throws CommandException; + /** * Returns the AddressBook. * @@ -30,9 +41,13 @@ public interface Logic { */ ReadOnlyAddressBook getAddressBook(); - /** Returns an unmodifiable view of the filtered list of persons */ + /** + * Returns an unmodifiable view of the filtered list of persons + */ ObservableList getFilteredPersonList(); + ObservableList getSortedPersonList(); + /** * Returns the user prefs' address book file path. */ diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..837034f578d 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -46,9 +46,13 @@ public LogicManager(Model model, Storage storage) { public CommandResult execute(String commandText) throws CommandException, ParseException { logger.info("----------------[USER COMMAND][" + commandText + "]"); - CommandResult commandResult; Command command = addressBookParser.parseCommand(commandText); - commandResult = command.execute(model); + return execute(command); + } + + @Override + public CommandResult execute(Command command) throws CommandException { + CommandResult commandResult = command.execute(model); try { storage.saveAddressBook(model.getAddressBook()); @@ -71,6 +75,11 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getSortedPersonList() { + return model.getSortedPersonList(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..cf2b4e6bdb6 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -17,7 +17,14 @@ public class Messages { 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_DUPLICATE_FIELDS = - "Multiple values specified for the following single-valued field(s): "; + "Multiple values specified for the following single-valued field(s): "; + public static final String MESSAGE_SORTED_OVERVIEW = "List has been sorted by %s."; + + public static final String MESSAGE_SORT_CLEARED = "List has been sorted by default order."; + public static final String MESSAGE_INVALID_SORT_TYPE = "%s is not a valid sort type!"; + + public static final String MESSAGE_INVALID_MOBILE = "The person must have a Singaporean number" + + " (8-digit number starting with 8 or 9)."; /** * Returns an error message indicating the duplicate prefixes. diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..1a9b5c997bf 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -2,7 +2,10 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAYS_AVAILABLE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY_OWED; 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; @@ -26,14 +29,19 @@ public class AddCommand extends Command { + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_BIRTHDAY + "BIRTHDAY] " + + "[" + PREFIX_MONEY_OWED + "MONEY_OWED] " + + "[" + PREFIX_TAG + "TAG]... " + + "[" + PREFIX_DAYS_AVAILABLE + "DAY]...\n" + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "98765432 " + PREFIX_EMAIL + "johnd@example.com " + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " + + PREFIX_MONEY_OWED + "24.50 " + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_TAG + "owesMoney " + + PREFIX_DAYS_AVAILABLE + "THURSDAY"; 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"; @@ -57,7 +65,8 @@ public CommandResult execute(Model model) throws CommandException { } model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))) + .withPersonToShow(model.findIndex(toAdd)); } @Override diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 249b6072d0d..0191b459254 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -5,6 +5,7 @@ import java.util.Objects; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; /** * Represents the result of a command execution. @@ -13,19 +14,49 @@ public class CommandResult { private final String feedbackToUser; - /** Help information should be shown to the user. */ + /** + * Help information should be shown to the user. + */ private final boolean showHelp; - /** The application should exit. */ + /** + * The application should exit. + */ private final boolean exit; + /** + * Used for showing QR code for the person to be paid. + */ + private final Person personToPay; + + /** + * Used for setting the UI to display the person at the current index. If an invalid + * index is given, UI will display the HomeCard instead. + */ + private final Integer personToShow; + /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult( + String feedbackToUser, boolean showHelp, boolean exit, Person personToPay, Integer personToShow) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.personToPay = personToPay; + this.personToShow = personToShow; + } + + /** + * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, {@code showHelp} + * and {@code exit}, and other fields set to their default value. + */ + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + this(feedbackToUser, showHelp, exit, null, null); + } + + public CommandResult(String feedbackToUser, Person personToPay) { + this(feedbackToUser, false, false, personToPay, null); } /** @@ -33,7 +64,7 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { * and other fields set to their default value. */ public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + this(feedbackToUser, false, false, null, null); } public String getFeedbackToUser() { @@ -48,6 +79,31 @@ public boolean isExit() { return exit; } + public boolean isShowPayment() { + return personToPay != null; + } + + public Person getPersonToPay() { + return personToPay; + } + + public Integer getPersonToShow() { + return personToShow; + } + + /** + * Returns a new {@code CommandResult} with the personToShow set to the provided index. + */ + public CommandResult withPersonToShow(int index) { + return new CommandResult( + feedbackToUser, + showHelp, + exit, + personToPay, + index + ); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -62,12 +118,14 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && exit == otherCommandResult.exit + && Objects.equals(personToPay, otherCommandResult.personToPay) + && Objects.equals(personToShow, otherCommandResult.personToShow); } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, showHelp, exit, personToPay, personToShow); } @Override @@ -76,6 +134,8 @@ public String toString() { .add("feedbackToUser", feedbackToUser) .add("showHelp", showHelp) .add("exit", exit) + .add("personToPay", personToPay) + .add("personToShow", personToShow) .toString(); } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..05d9bcef92c 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -2,7 +2,10 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAYS_AVAILABLE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY_OWED; 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; @@ -22,10 +25,14 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; +import seedu.address.model.person.Day; import seedu.address.model.person.Email; +import seedu.address.model.person.MoneyOwed; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Remark; import seedu.address.model.tag.Tag; /** @@ -43,7 +50,10 @@ public class EditCommand extends Command { + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_TAG + "TAG]... " + + "[" + PREFIX_BIRTHDAY + "BIRTHDAY] " + + "[" + PREFIX_MONEY_OWED + "MONEY_OWED]\n" + + "[" + PREFIX_DAYS_AVAILABLE + "DAY]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " + PREFIX_EMAIL + "johndoe@example.com"; @@ -56,7 +66,7 @@ public class EditCommand extends Command { private final EditPersonDescriptor editPersonDescriptor; /** - * @param index of the person in the filtered person list to edit + * @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) { @@ -67,6 +77,28 @@ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); } + /** + * 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()); + Remark updatedRemark = personToEdit.getRemark(); // edit command does not allow editing remarks + Birthday updatedBirthday = editPersonDescriptor.getBirthday().orElse(personToEdit.getBirthday()); + MoneyOwed updatedMoneyOwed = editPersonDescriptor.getMoneyOwed().orElse(personToEdit.getMoneyOwed()); + Set updatedDaysAvailable = editPersonDescriptor + .getDaysAvailable().orElse(personToEdit.getDaysAvailable()); + + return new Person(updatedName, updatedPhone, updatedEmail, + updatedAddress, updatedRemark, updatedTags, updatedBirthday, updatedMoneyOwed, updatedDaysAvailable); + } + @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); @@ -85,23 +117,9 @@ public CommandResult execute(Model model) throws CommandException { model.setPerson(personToEdit, editedPerson); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(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); + return new CommandResult( + String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))) + .withPersonToShow(model.findIndex(editedPerson)); } @Override @@ -138,8 +156,12 @@ public static class EditPersonDescriptor { private Email email; private Address address; private Set tags; + private Birthday birthday; + private MoneyOwed moneyOwed; + private Set daysAvailable; - public EditPersonDescriptor() {} + public EditPersonDescriptor() { + } /** * Copy constructor. @@ -151,45 +173,73 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setEmail(toCopy.email); setAddress(toCopy.address); setTags(toCopy.tags); + setBirthday(toCopy.birthday); + setMoneyOwed(toCopy.moneyOwed); + setDaysAvailable(toCopy.daysAvailable); + } + + public Optional getBirthday() { + return Optional.ofNullable(birthday); + } + + public void setBirthday(Birthday birthday) { + this.birthday = birthday; } /** * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, address, tags, birthday, moneyOwed, daysAvailable); + } + + public Optional getName() { + return Optional.ofNullable(name); } public void setName(Name name) { this.name = name; } - public Optional getName() { - return Optional.ofNullable(name); + public Optional getPhone() { + return Optional.ofNullable(phone); } public void setPhone(Phone phone) { this.phone = phone; } - public Optional getPhone() { - return Optional.ofNullable(phone); + public Optional getEmail() { + return Optional.ofNullable(email); } public void setEmail(Email email) { this.email = email; } - public Optional getEmail() { - return Optional.ofNullable(email); + public Optional
getAddress() { + return Optional.ofNullable(address); } public void setAddress(Address address) { this.address = address; } - public Optional
getAddress() { - return Optional.ofNullable(address); + public Optional getMoneyOwed() { + return Optional.ofNullable(moneyOwed); + } + + public void setMoneyOwed(MoneyOwed moneyOwed) { + this.moneyOwed = moneyOwed; + } + + /** + * 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(); } /** @@ -201,12 +251,20 @@ public void setTags(Set tags) { } /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * Returns an unmodifiable days set, which throws {@code UnsupportedOperationException} * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. + * Returns {@code Optional#empty()} if {@code dayAvailable} is null. */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + public Optional> getDaysAvailable() { + return (daysAvailable != null) ? Optional.of(Collections.unmodifiableSet(daysAvailable)) : Optional.empty(); + } + + /** + * Sets {@code daysAvailable} to this object's {@code daysAvailable}. + * A defensive copy of {@code daysAvailable} is used internally. + */ + public void setDaysAvailable(Set daysAvailable) { + this.daysAvailable = (daysAvailable != null) ? new HashSet<>(daysAvailable) : null; } @Override @@ -225,7 +283,10 @@ public boolean equals(Object other) { && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) && Objects.equals(address, otherEditPersonDescriptor.address) - && Objects.equals(tags, otherEditPersonDescriptor.tags); + && Objects.equals(tags, otherEditPersonDescriptor.tags) + && Objects.equals(birthday, otherEditPersonDescriptor.birthday) + && Objects.equals(moneyOwed, otherEditPersonDescriptor.moneyOwed) + && Objects.equals(daysAvailable, otherEditPersonDescriptor.daysAvailable); } @Override @@ -235,7 +296,10 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("birthday", birthday) .add("tags", tags) + .add("moneyOwed", moneyOwed) + .add("daysAvailable", daysAvailable) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/FilterCommand.java b/src/main/java/seedu/address/logic/commands/FilterCommand.java new file mode 100644 index 00000000000..467d827490a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FilterCommand.java @@ -0,0 +1,68 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * A common superclass for all filter commands that have the same logic, but filter using + * different predicates. + */ +public abstract class FilterCommand extends Command { + public static final String COMMAND_WORD = "filter"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filters the contact list according to one of the " + + "three possible types: name, days available or tags and displays them as a list with index numbers.\n" + + "Parameters: filter TYPE [KEYWORDS]... [--all]\n" + + "Example 1: " + COMMAND_WORD + " tag student" + + "Example 2: " + COMMAND_WORD + " day monday tuesday --all"; + + private final Predicate predicate; + + /** + * Helps subclasses of filter to set appropriate predicates to filter for different + * fields. + * + * @param predicate to be assigned to filter object + */ + + public FilterCommand(Predicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())) + .withPersonToShow(Model.INVALID_PERSON_INDEX); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FilterCommand)) { + return false; + } + + FilterCommand otherFilterCommand = (FilterCommand) other; + return predicate.equals(otherFilterCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/FilterDayCommand.java b/src/main/java/seedu/address/logic/commands/FilterDayCommand.java new file mode 100644 index 00000000000..3fa2b46c2bd --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FilterDayCommand.java @@ -0,0 +1,26 @@ +package seedu.address.logic.commands; + +import seedu.address.model.person.predicates.PersonAvailableOnDayPredicate; + +/** + * Filters and lists all persons in address book who are available on any of the given days of the week. + * Day matching is case-insensitive. + */ +public class FilterDayCommand extends FilterCommand { + + public static final String TYPE = "day"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filters for all persons whose availabilities" + + " include any of the specified days and displays them as a list with index numbers.\n" + + "Parameters: filter day [days of the week]...\n" + + "Example: " + COMMAND_WORD + " " + TYPE + " monday"; + + /** + * Returns a new FilterDayCommand object that takes in a PersonAvailableOnDayPredicate + * to update the filtered list + * @param predicate + */ + public FilterDayCommand(PersonAvailableOnDayPredicate predicate) { + super(predicate); + } +} diff --git a/src/main/java/seedu/address/logic/commands/FilterNameCommand.java b/src/main/java/seedu/address/logic/commands/FilterNameCommand.java new file mode 100644 index 00000000000..e26f6aa057d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FilterNameCommand.java @@ -0,0 +1,21 @@ +package seedu.address.logic.commands; + +import seedu.address.model.person.predicates.NameContainsKeywordsPredicate; + +/** + * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Keyword matching is case-insensitive. + */ +public class FilterNameCommand extends FilterCommand { + + public static final String TYPE = "name"; + + 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... [--all]\n" + + "Example: " + COMMAND_WORD + " " + TYPE + " alice bob charlie"; + + public FilterNameCommand(NameContainsKeywordsPredicate predicate) { + super(predicate); + } +} diff --git a/src/main/java/seedu/address/logic/commands/FilterTagCommand.java b/src/main/java/seedu/address/logic/commands/FilterTagCommand.java new file mode 100644 index 00000000000..adbc1bfcc6e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FilterTagCommand.java @@ -0,0 +1,26 @@ +package seedu.address.logic.commands; + +import seedu.address.model.person.predicates.PersonHasTagPredicate; + +/** + * Filters and lists all persons in address book who are tagged by any of the argument keywords. + * Keyword matching is case-insensitive. + */ +public class FilterTagCommand extends FilterCommand { + + public static final String TYPE = "tag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filters for all persons whose tags include any of " + + "the specified keywords and displays them as a list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " " + TYPE + " student"; + + /** + * Returns a new FilterTagCommand object that takes in a PersonHasTagPredicate + * to update the filtered list + * @param predicate + */ + public FilterTagCommand(PersonHasTagPredicate predicate) { + super(predicate); + } +} 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 72b9eddd3a7..00000000000 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ /dev/null @@ -1,58 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.util.ToStringBuilder; -import seedu.address.logic.Messages; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. - */ -public class FindCommand extends Command { - - public static final String COMMAND_WORD = "find"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; - - private final NameContainsKeywordsPredicate predicate; - - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; - } - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof FindCommand)) { - return false; - } - - FindCommand otherFindCommand = (FindCommand) other; - return predicate.equals(otherFindCommand.predicate); - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .add("predicate", predicate) - .toString(); - } -} diff --git a/src/main/java/seedu/address/logic/commands/LendCommand.java b/src/main/java/seedu/address/logic/commands/LendCommand.java new file mode 100644 index 00000000000..1b0d5da4ce2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LendCommand.java @@ -0,0 +1,103 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY_OWED; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.MoneyOwed; +import seedu.address.model.person.Person; + +/** + * Lends an amount of money on top of current amount to a person identified + * using the displayed index from the address book. + */ +public class LendCommand extends Command { + + public static final String COMMAND_WORD = "lend"; + public static final String MESSAGE_LENT_PERSON_SUCCESS = + "Lend to person %1$s"; + public static final String MESSAGE_MISSING_AMOUNT = + "Please enter an amount that you want to lend!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Lend an amount of money on top of the current amount owed of a person " + + "using the displayed index from the address book.\n" + + "Maximum amount you can lend is $100,000.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_MONEY_OWED + "MONEY_OWED " + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_MONEY_OWED + "4.50"; + private final Index targetIndex; + private final MoneyOwed amountToLend; + + /** + * Returns a new LendCommand object that takes in an Index object + * and a MoneyOwed object. + * + * @param targetIndex index of person to lend. + * @param amountToLend amount to lend to the person. + */ + public LendCommand(Index targetIndex, MoneyOwed amountToLend) { + requireNonNull(targetIndex); + requireNonNull(amountToLend); + this.targetIndex = targetIndex; + this.amountToLend = amountToLend; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + Person personToLend = lastShownList.get(targetIndex.getZeroBased()); + Person lentPerson; + try { + lentPerson = new Person( + personToLend.getName(), personToLend.getPhone(), personToLend.getEmail(), + personToLend.getAddress(), personToLend.getRemark(), personToLend.getTags(), + personToLend.getBirthday(), + personToLend.getMoneyOwed().addAmountOwed(amountToLend.getAmount()), + personToLend.getDaysAvailable()); + } catch (IllegalArgumentException e) { + throw new CommandException(MoneyOwed.MESSAGE_CONSTRAINTS); + } + + model.setPerson(personToLend, lentPerson); + model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult( + String.format(MESSAGE_LENT_PERSON_SUCCESS, Messages.format(lentPerson))) + .withPersonToShow(model.findIndex(lentPerson)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof LendCommand)) { + return false; + } + + LendCommand otherLendCommand = (LendCommand) other; + return targetIndex.equals(otherLendCommand.targetIndex) + && amountToLend.equals(otherLendCommand.amountToLend); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .add("amountToLend", amountToLend) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..a2eab49f15a 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -19,6 +19,7 @@ public class ListCommand extends Command { public CommandResult execute(Model model) { requireNonNull(model); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); + return new CommandResult(MESSAGE_SUCCESS) + .withPersonToShow(Model.INVALID_PERSON_INDEX); } } diff --git a/src/main/java/seedu/address/logic/commands/PayCommand.java b/src/main/java/seedu/address/logic/commands/PayCommand.java new file mode 100644 index 00000000000..20507e164ec --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/PayCommand.java @@ -0,0 +1,75 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_MOBILE; + +import java.util.List; +import java.util.Objects; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Generates a QR code and shows it to the user if the person has a valid Singapore phone number. + * The QR code can be scanned by a banking application to transfer money to the person. + */ +public class PayCommand extends Command { + + public static final String COMMAND_WORD = "pay"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Generates a PayNow QR code for the person identified by the index number used " + + "in the displayed person list. The person must have a valid Singapore phone number.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_GENERATE_QR_SUCCESS = "Generated QR code for Person: %1$s"; + + private final Index targetIndex; + + public PayCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToPay = lastShownList.get(targetIndex.getZeroBased()); + if (!personToPay.getPhone().isSingaporeanNumber()) { + throw new CommandException(MESSAGE_INVALID_MOBILE); + } + return new CommandResult( + String.format(MESSAGE_GENERATE_QR_SUCCESS, Messages.format(personToPay)), + personToPay) + .withPersonToShow(model.findIndex(personToPay)); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PayCommand)) { + return false; + } + PayCommand otherCommand = (PayCommand) other; + return Objects.equals(targetIndex, otherCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RemarkCommand.java b/src/main/java/seedu/address/logic/commands/RemarkCommand.java new file mode 100644 index 00000000000..8a573715df0 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RemarkCommand.java @@ -0,0 +1,94 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.person.Remark; + +/** + * Changes the remark of an existing person in the address book. + */ +public class RemarkCommand extends Command { + + public static final String COMMAND_WORD = "remark"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Edits the remark of the person identified " + + "by the index number used in the last person listing. " + + "Existing remark will be overwritten by the input.\n" + + "Parameters: INDEX (must be a positive integer) " + + "r/ [REMARK]\n" + + "Example: " + COMMAND_WORD + " 1 " + + "r/ Likes to swim."; + + public static final String MESSAGE_ADD_REMARK_SUCCESS = "Added remark to Person: %1$s"; + public static final String MESSAGE_DELETE_REMARK_SUCCESS = "Removed remark from Person: %1$s"; + + private final Index index; + private final Remark remark; + + /** + * @param index index of the contact + * @param remark string representing remark of contact + */ + public RemarkCommand(Index index, Remark remark) { + requireAllNonNull(index, remark.toString()); + + this.index = index; + this.remark = remark; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + Person editedPerson = new Person( + personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), personToEdit.getAddress(), + remark, personToEdit.getTags(), personToEdit.getBirthday(), personToEdit.getMoneyOwed(), + personToEdit.getDaysAvailable()); + + model.setPerson(personToEdit, editedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + return new CommandResult(generateSuccessMessage(editedPerson)) + .withPersonToShow(model.findIndex(editedPerson)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof RemarkCommand)) { + return false; + } + + RemarkCommand e = (RemarkCommand) other; + return index.equals(e.index) + && remark.equals(e.remark); + } + + /** + * Generates a command execution success message based on whether + * the remark is added to or removed from + * {@code personToEdit}. + */ + private String generateSuccessMessage(Person personToEdit) { + String message = !remark.value.isEmpty() ? MESSAGE_ADD_REMARK_SUCCESS : MESSAGE_DELETE_REMARK_SUCCESS; + return String.format(message, personToEdit); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ResetDebtCommand.java b/src/main/java/seedu/address/logic/commands/ResetDebtCommand.java new file mode 100644 index 00000000000..9d3ba20cbab --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ResetDebtCommand.java @@ -0,0 +1,74 @@ +package seedu.address.logic.commands; + +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.MoneyOwed; +import seedu.address.model.person.Person; + +/** + * Resets the moneyOwed of a Person to 0. This is called by a button click instead of a CLI command. + */ +public class ResetDebtCommand extends Command { + public static final String PERSON_NOT_FOUND_MESSAGE = "The person with the number %s cannot be" + + " found in the address book!"; + public static final String RESET_SUCCESS_MESSAGE = "Reset money owed to %s to $0."; + private final Person originalPerson; + + public ResetDebtCommand(Person originalPerson) { + this.originalPerson = originalPerson; + } + + private Person resetPersonDebt(Person person) { + return new Person(person.getName(), person.getPhone(), person.getEmail(), + person.getAddress(), person.getRemark(), person.getTags(), person.getBirthday(), new MoneyOwed("0"), + person.getDaysAvailable()); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + Optional personMaybe = model.findPerson( + person -> person.getPhone().equals(originalPerson.getPhone())); + Person person; + try { + person = personMaybe.get(); + } catch (NoSuchElementException e) { + throw new CommandException(String.format(PERSON_NOT_FOUND_MESSAGE, originalPerson.getPhone())); + } + if (person.getMoneyOwed().moneyOwed == 0) { + return new CommandResult(String.format(RESET_SUCCESS_MESSAGE, person.getName())) + .withPersonToShow(model.findIndex(person)); + } + Person editedPerson = resetPersonDebt(person); + model.setPerson(person, editedPerson); + int personIndex = model.findIndex(editedPerson); + CommandResult result = new CommandResult(String.format(RESET_SUCCESS_MESSAGE, person.getName())); + if (personIndex == Model.INVALID_PERSON_INDEX) { + return result; + } + return result.withPersonToShow(personIndex); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ResetDebtCommand)) { + return false; + } + ResetDebtCommand otherCommand = (ResetDebtCommand) other; + return Objects.equals(originalPerson, otherCommand.originalPerson); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("originalPerson", originalPerson) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SortCommand.java b/src/main/java/seedu/address/logic/commands/SortCommand.java new file mode 100644 index 00000000000..08097a79b40 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SortCommand.java @@ -0,0 +1,99 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.Messages.MESSAGE_SORT_CLEARED; + +import java.util.Comparator; +import java.util.Objects; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Birthday; +import seedu.address.model.person.MoneyOwed; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.exceptions.InvalidSortTypeException; + +/** + * Sorts the address book in some specified order. + */ +public class SortCommand extends Command { + public static final String COMMAND_WORD = "sort"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sorts your contacts according" + + "to the specified sorting method.\n" + + "Available methods:\n" + + "1. name\n" + + "2. birthday\n" + + "3. money\n" + + "4. clear\n" + + "Example: " + COMMAND_WORD + " birthday"; + public static final String BIRTHDAY_SORT_TYPE = "birthday"; + public static final String NAME_SORT_TYPE = "name"; + public static final String MONEY_SORT_TYPE = "money"; + public static final String CLEAR_SORT_TYPE = "clear"; + private final String sortType; + private final Comparator personComparator; + + /** + * Returns a new SortCommand object that takes in a {@code Comparator} to + * sort the address book. + */ + public SortCommand(String sortType) throws InvalidSortTypeException { + switch (sortType.toLowerCase()) { + case BIRTHDAY_SORT_TYPE: + this.personComparator = Birthday.BIRTHDAY_COMPARATOR; + break; + case NAME_SORT_TYPE: + this.personComparator = Name.NAME_COMPARATOR; + break; + case MONEY_SORT_TYPE: + this.personComparator = MoneyOwed.MONEY_COMPARATOR; + break; + case CLEAR_SORT_TYPE: + this.personComparator = null; + break; + default: + throw new InvalidSortTypeException(sortType); + } + this.sortType = sortType; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + model.updatePersonComparator(personComparator); + return new CommandResult( + personComparator == null + ? MESSAGE_SORT_CLEARED + : String.format( + Messages.MESSAGE_SORTED_OVERVIEW, sortType + ) + ); + } + + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof SortCommand)) { + return false; + } + + SortCommand otherSort = (SortCommand) other; + + if (!Objects.equals(sortType, otherSort.sortType)) { + return false; + } + return Objects.equals(personComparator, otherSort.personComparator); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("sortType", sortType) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SplitCommand.java b/src/main/java/seedu/address/logic/commands/SplitCommand.java new file mode 100644 index 00000000000..ecf0df14cc2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SplitCommand.java @@ -0,0 +1,156 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY_OWED; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.MoneyOwed; +import seedu.address.model.person.Person; + +/** + * Splits the sum of money owed among user and a group of people using the displayed + * index from the address book. + */ +public class SplitCommand extends Command { + public static final String COMMAND_WORD = "split"; + public static final Float MINIMUM_SPLIT_AMOUNT = (float) 0.01; + public static final String MESSAGE_INVALID_AMOUNT = + "Amount after splitting should be more than $0.01!"; + public static final String MESSAGE_MISSING_AMOUNT = + "Please enter an amount that you want to split!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Splits the sum of money owed among you and a group of people " + + "using the displayed index from the address book.\n" + + "Maximum amount you can split is $100,000." + + "Parameters: at least one INDEX (must be a positive integer) " + + PREFIX_MONEY_OWED + "MONEY_OWED " + + "Example: " + COMMAND_WORD + " 1 2 " + + PREFIX_MONEY_OWED + "4.50"; + private final List indexListToSplit; + private final MoneyOwed totalOwed; + + /** + * Returns a new SplitCommand object that takes in a list of index + * and a MoneyOwed object. + * + * @param indexListToSplit list of index of person to split with. + * @param totalOwed total amount to split. + */ + public SplitCommand(List indexListToSplit, MoneyOwed totalOwed) { + this.indexListToSplit = indexListToSplit; + this.totalOwed = totalOwed; + } + + /** + * Splits the total amount of a group of people. + * + * @param totalAmount total amount to split. + * @param numPeople number of people to split with. + * @return the split amount + */ + public static Float getSplitAmount(Float totalAmount, int numPeople) { + String splitAmountRounded = String.format("%.2f", totalAmount / numPeople); + Float splitAmount = Float.parseFloat(splitAmountRounded); + return splitAmount; + } + + /** + * Checks if the index list is valid. + * + * @param indexList list of index of person to split with. + * @param sizeOfLastShownList size of last displayed list. + * @return true if each index in index list is valid. + */ + public static boolean hasValidIndexList(List indexList, int sizeOfLastShownList) { + for (Index index : indexList) { + if (index.getZeroBased() >= sizeOfLastShownList) { + return false; + } + } + return true; + } + + /** + * Checks if all person in the split does not exceed the minimum and maximum amount. + * + * @param lastShownList list of person in the display list. + * @param indexList list of index to split amount with. + * @param splitAmount amount to be added to each person. + * @return true if each person money owed does not exceed limit after split. + */ + public static boolean hasValidAmountAfterSplit(List lastShownList, List indexList, + Float splitAmount) { + try { + for (Index index : indexList) { + Person person = lastShownList.get(index.getZeroBased()); + person.getMoneyOwed().addAmountOwed(splitAmount); + } + } catch (IllegalArgumentException e) { + return false; + } + return true; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + Float splitAmount = getSplitAmount(totalOwed.getAmount(), indexListToSplit.size() + 1); + + if (splitAmount < MINIMUM_SPLIT_AMOUNT) { + throw new CommandException(MESSAGE_INVALID_AMOUNT); + } + if (!hasValidIndexList(indexListToSplit, lastShownList.size())) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + if (!hasValidAmountAfterSplit(lastShownList, indexListToSplit, splitAmount)) { + throw new CommandException(MoneyOwed.MESSAGE_CONSTRAINTS); + } + + for (Index index : indexListToSplit) { + Person personToEdit = lastShownList.get(index.getZeroBased()); + Person editedPerson = new Person( + personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), personToEdit.getRemark(), personToEdit.getTags(), + personToEdit.getBirthday(), personToEdit.getMoneyOwed().addAmountOwed(splitAmount), + personToEdit.getDaysAvailable()); + + model.setPerson(personToEdit, editedPerson); + } + model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + + return new CommandResult( + String.format("$%s has been split among you and %d more people!", + totalOwed, indexListToSplit.size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SplitCommand)) { + return false; + } + + SplitCommand otherSplitCommand = (SplitCommand) other; + return indexListToSplit.equals(otherSplitCommand.indexListToSplit) + && totalOwed.equals(otherSplitCommand.totalOwed); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("indexListToSplit", indexListToSplit) + .add("totalOwed", totalOwed) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..565584797a8 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -2,7 +2,10 @@ import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAYS_AVAILABLE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY_OWED; 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; @@ -13,10 +16,14 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; +import seedu.address.model.person.Day; import seedu.address.model.person.Email; +import seedu.address.model.person.MoneyOwed; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Remark; import seedu.address.model.tag.Tag; /** @@ -27,11 +34,13 @@ public class AddCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the AddCommand * and returns an AddCommand object for execution. + * * @throws ParseException if the user input does not conform the expected format */ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG, + PREFIX_BIRTHDAY, PREFIX_MONEY_OWED, PREFIX_DAYS_AVAILABLE); if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) || !argMultimap.getPreamble().isEmpty()) { @@ -43,9 +52,13 @@ public AddCommand parse(String args) throws ParseException { Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + Remark remark = new Remark(""); // add command does not allow adding remarks straight away Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + Birthday birthday = ParserUtil.parseBirthday(argMultimap.getValue(PREFIX_BIRTHDAY).orElse("")); + MoneyOwed moneyOwed = ParserUtil.parseMoneyOwed(argMultimap.getValue(PREFIX_MONEY_OWED).orElse("0")); + Set daysAvailable = ParserUtil.parseDays(argMultimap.getAllValues(PREFIX_DAYS_AVAILABLE)); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, email, address, remark, tagList, birthday, moneyOwed, daysAvailable); return new AddCommand(person); } diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..71d920ee024 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -14,9 +14,14 @@ import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FilterCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.LendCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.PayCommand; +import seedu.address.logic.commands.RemarkCommand; +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.commands.SplitCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -65,9 +70,6 @@ public Command parseCommand(String userInput) throws ParseException { case ClearCommand.COMMAND_WORD: return new ClearCommand(); - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - case ListCommand.COMMAND_WORD: return new ListCommand(); @@ -77,6 +79,24 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case RemarkCommand.COMMAND_WORD: + return new RemarkCommandParser().parse(arguments); + + case FilterCommand.COMMAND_WORD: + return new FilterCommandParser().parse(arguments); + + case SortCommand.COMMAND_WORD: + return new SortCommandParser().parse(arguments); + + case SplitCommand.COMMAND_WORD: + return new SplitCommandParser().parse(arguments); + + case PayCommand.COMMAND_WORD: + return new PayCommandParser().parse(arguments); + + case LendCommand.COMMAND_WORD: + return new LendCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java index 21e26887a83..b4dd2ac5ac6 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java @@ -19,7 +19,9 @@ */ public class ArgumentMultimap { - /** Prefixes mapped to their respective arguments**/ + /** + * Prefixes mapped to their respective arguments + **/ private final Map> argMultimap = new HashMap<>(); /** diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..17e43558c83 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -10,6 +10,10 @@ public class CliSyntax { public static final Prefix PREFIX_PHONE = new Prefix("p/"); public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_MONEY_OWED = new Prefix("$/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_REMARK = new Prefix("r/"); + public static final Prefix PREFIX_BIRTHDAY = new Prefix("b/"); + public static final Prefix PREFIX_DAYS_AVAILABLE = new Prefix("d/"); } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..3955cf36080 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -3,7 +3,10 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAYS_AVAILABLE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY_OWED; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -17,6 +20,7 @@ import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Day; import seedu.address.model.tag.Tag; /** @@ -27,12 +31,14 @@ public class EditCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the EditCommand * and returns an EditCommand object for execution. + * * @throws ParseException if the user input does not conform the expected format */ public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_TAG, PREFIX_BIRTHDAY, PREFIX_MONEY_OWED, PREFIX_DAYS_AVAILABLE); Index index; @@ -42,7 +48,8 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_BIRTHDAY, PREFIX_MONEY_OWED); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -59,7 +66,14 @@ public EditCommand parse(String args) throws ParseException { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); - + if (argMultimap.getValue(PREFIX_BIRTHDAY).isPresent()) { + editPersonDescriptor.setBirthday(ParserUtil.parseBirthday(argMultimap.getValue(PREFIX_BIRTHDAY).get())); + } + if (argMultimap.getValue(PREFIX_MONEY_OWED).isPresent()) { + editPersonDescriptor.setMoneyOwed(ParserUtil.parseMoneyOwed(argMultimap.getValue(PREFIX_MONEY_OWED).get())); + } + parseDaysAvailableForEdit(argMultimap.getAllValues(PREFIX_DAYS_AVAILABLE)) + .ifPresent(editPersonDescriptor::setDaysAvailable); if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); } @@ -82,4 +96,14 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars return Optional.of(ParserUtil.parseTags(tagSet)); } + private Optional> parseDaysAvailableForEdit(Collection days) throws ParseException { + assert days != null; + + if (days.isEmpty()) { + return Optional.empty(); + } + Collection daySet = days.size() == 1 && days.contains("") ? Collections.emptySet() : days; + return Optional.of(ParserUtil.parseDays(daySet)); + } + } diff --git a/src/main/java/seedu/address/logic/parser/FilterCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java new file mode 100644 index 00000000000..a4e8c152117 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java @@ -0,0 +1,84 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import seedu.address.logic.commands.FilterCommand; +import seedu.address.logic.commands.FilterDayCommand; +import seedu.address.logic.commands.FilterNameCommand; +import seedu.address.logic.commands.FilterTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Day; +import seedu.address.model.person.predicates.NameContainsKeywordsPredicate; +import seedu.address.model.person.predicates.PersonAvailableOnDayPredicate; +import seedu.address.model.person.predicates.PersonHasTagPredicate; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new FilterCommand object + */ +public class FilterCommandParser implements Parser { + private static final Prefix FLAG_ALL = new Prefix("--all"); + + /** + * Parses the given {@code String} of arguments in the context of the FilterTagCommand + * and returns a FilterTagCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public FilterCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + String argsWithoutType = ""; + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, FLAG_ALL); + boolean matchAll = argMultimap.getValue(FLAG_ALL).isPresent(); + trimmedArgs = argMultimap.getPreamble(); + + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + + if (trimmedArgs.toLowerCase().startsWith(FilterDayCommand.TYPE)) { + argsWithoutType = trimmedArgs.replaceFirst(FilterDayCommand.TYPE, "").trim(); + if (argsWithoutType.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FilterDayCommand.MESSAGE_USAGE)); + } + + Set getDays = ParserUtil.parseDays(Arrays.asList(argsWithoutType.split("\\s+"))); + return new FilterDayCommand(new PersonAvailableOnDayPredicate(new ArrayList<>(getDays), matchAll)); + } + + if (trimmedArgs.toLowerCase().startsWith(FilterTagCommand.TYPE)) { + argsWithoutType = trimmedArgs.replaceFirst(FilterTagCommand.TYPE, "").trim(); + if (argsWithoutType.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FilterTagCommand.MESSAGE_USAGE)); + } + + Set tags = ParserUtil.parseTags(Arrays.asList(argsWithoutType.split("\\s+"))); + return new FilterTagCommand(new PersonHasTagPredicate(new ArrayList<>(tags), matchAll)); + } + + if (trimmedArgs.toLowerCase().startsWith(FilterNameCommand.TYPE)) { + argsWithoutType = trimmedArgs.replaceFirst(FilterNameCommand.TYPE, "").trim(); + if (argsWithoutType.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FilterNameCommand.MESSAGE_USAGE)); + } + + return new FilterNameCommand( + new NameContainsKeywordsPredicate(List.of(argsWithoutType.split("\\s+")), matchAll)); + } + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } +} 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 2867bde857b..00000000000 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ /dev/null @@ -1,33 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import java.util.Arrays; - -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Parses input arguments and creates a new FindCommand object - */ -public class FindCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the FindCommand - * and returns a FindCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } - - String[] nameKeywords = trimmedArgs.split("\\s+"); - - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/LendCommandParser.java b/src/main/java/seedu/address/logic/parser/LendCommandParser.java new file mode 100644 index 00000000000..3e95b459cf7 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/LendCommandParser.java @@ -0,0 +1,46 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY_OWED; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.LendCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.MoneyOwed; + +/** + * Parses input arguments and creates a new LendCommand object + */ +public class LendCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the LendCommand + * and returns a LendCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public LendCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_MONEY_OWED); + Index targetIndex; + MoneyOwed lentAmount; + try { + targetIndex = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, LendCommand.MESSAGE_USAGE), + pe); + } + + if (!argMultimap.getValue(PREFIX_MONEY_OWED).isPresent()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, LendCommand.MESSAGE_MISSING_AMOUNT)); + } + + lentAmount = ParserUtil.parseMoneyOwed(argMultimap.getValue(PREFIX_MONEY_OWED).get()); + + return new LendCommand(targetIndex, lentAmount); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..b979987d71c 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -10,7 +10,10 @@ import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; +import seedu.address.model.person.Day; import seedu.address.model.person.Email; +import seedu.address.model.person.MoneyOwed; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -25,6 +28,7 @@ public class ParserUtil { /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be * trimmed. + * * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). */ public static Index parseIndex(String oneBasedIndex) throws ParseException { @@ -95,6 +99,21 @@ public static Email parseEmail(String email) throws ParseException { return new Email(trimmedEmail); } + /** + * Parses a {@code String moneyOwed} into a {@code MoneyOwed}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code MoneyOwed} is invalid. + */ + public static MoneyOwed parseMoneyOwed(String moneyOwed) throws ParseException { + requireNonNull(moneyOwed); + String trimmedMoneyOwed = moneyOwed.trim(); + if (!MoneyOwed.isValidMoney(trimmedMoneyOwed)) { + throw new ParseException(MoneyOwed.MESSAGE_CONSTRAINTS); + } + return new MoneyOwed(trimmedMoneyOwed); + } + /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. @@ -121,4 +140,42 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses {@code String birthday} into a {@code Birthday}. + */ + public static Birthday parseBirthday(String birthday) throws ParseException { + birthday = birthday == null ? "" : birthday; + String trimmedBirthday = birthday.trim(); + if (!Birthday.isValidBirthday(trimmedBirthday)) { + throw new ParseException(Birthday.BIRTHDAY_CONSTRAINTS); + } + return new Birthday(trimmedBirthday); + } + + private static Day parseDay(String day) throws ParseException { + requireNonNull(day); + String trimmedTag = day.trim(); + if (!Day.isValidDay(trimmedTag)) { + throw new ParseException(Day.MESSAGE_CONSTRAINTS); + } + return Day.getDay(day); + } + + /** + * Takes a collection of Strings representing days of the week and + * returns a Set representation of that. + * @param days + * @return Set representing some subset of the 7 days of the week. + * @throws ParseException when any one of the given Strings in the days collection + * cannot be mapped to a Day. + */ + public static Set parseDays(Collection days) throws ParseException { + requireNonNull(days); + final Set daySet = new HashSet<>(); + for (String dayName : days) { + daySet.add(parseDay(dayName)); + } + return daySet; + } } diff --git a/src/main/java/seedu/address/logic/parser/PayCommandParser.java b/src/main/java/seedu/address/logic/parser/PayCommandParser.java new file mode 100644 index 00000000000..85bacafa165 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/PayCommandParser.java @@ -0,0 +1,26 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.PayCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses the given {@code String} of arguments in the context of the PayCommand + * and returns a PayCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ +public class PayCommandParser implements Parser { + @Override + public PayCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new PayCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, PayCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/RemarkCommandParser.java b/src/main/java/seedu/address/logic/parser/RemarkCommandParser.java new file mode 100644 index 00000000000..28f16ec376a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RemarkCommandParser.java @@ -0,0 +1,40 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_REMARK; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.RemarkCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Remark; + +/** + * Parser for the remark command + */ +public class RemarkCommandParser implements Parser { + + /** + * @param args argument to be parsed + * @return a RemarkCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public RemarkCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, + PREFIX_REMARK); + + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + RemarkCommand.MESSAGE_USAGE), ive); + } + + Remark remark = new Remark(argMultimap.getValue(PREFIX_REMARK).orElse("")); + + return new RemarkCommand(index, remark); + } +} diff --git a/src/main/java/seedu/address/logic/parser/SortCommandParser.java b/src/main/java/seedu/address/logic/parser/SortCommandParser.java new file mode 100644 index 00000000000..19548f8d35c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SortCommandParser.java @@ -0,0 +1,22 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new SortCommand object + */ +public class SortCommandParser implements Parser { + @Override + public SortCommand parse(String args) throws ParseException { + String sortType = args.replaceFirst(SortCommand.COMMAND_WORD, "").trim(); + if (sortType.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + + return new SortCommand(sortType); + } +} diff --git a/src/main/java/seedu/address/logic/parser/SplitCommandParser.java b/src/main/java/seedu/address/logic/parser/SplitCommandParser.java new file mode 100644 index 00000000000..024cb1a5701 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SplitCommandParser.java @@ -0,0 +1,49 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MONEY_OWED; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.SplitCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.MoneyOwed; + +/** + * Parses input arguments and creates a new SplitCommand object + */ +public class SplitCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SplitCommand + * and returns a SplitCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public SplitCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_MONEY_OWED); + List indexList = new ArrayList<>(); + MoneyOwed totalOwed; + try { + String[] indexArray = argMultimap.getPreamble().split(" "); + for (String s : indexArray) { + indexList.add(ParserUtil.parseIndex(s)); + } + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SplitCommand.MESSAGE_USAGE), pe); + } + + if (argMultimap.getValue(PREFIX_MONEY_OWED).isPresent()) { + totalOwed = ParserUtil.parseMoneyOwed(argMultimap.getValue(PREFIX_MONEY_OWED).get()); + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SplitCommand.MESSAGE_MISSING_AMOUNT)); + } + return new SplitCommand(indexList, totalOwed); + } + +} diff --git a/src/main/java/seedu/address/logic/paynow/MerchantAccountInformation.java b/src/main/java/seedu/address/logic/paynow/MerchantAccountInformation.java new file mode 100644 index 00000000000..fa98312c4ea --- /dev/null +++ b/src/main/java/seedu/address/logic/paynow/MerchantAccountInformation.java @@ -0,0 +1,24 @@ +package seedu.address.logic.paynow; + +/** + * Represents a Merchant Account Information field within a PayNow QR Code. + */ +public class MerchantAccountInformation extends PayNowPayload { + private static final String DOMAIN = "SG.PAYNOW"; + private static final int DOMAIN_ID = 0; + private static final int PROXY_TYPE_ID = 1; + private static final int MOBILE_NUM_PROXY = 0; + private static final int MOBILE_NO_ID = 2; + private static final String SG_COUNTRY_CODE = "+65"; + private static final int EDITABLE_ID = 3; + private static final int EDITABLE = 1; + + protected MerchantAccountInformation(String phone) { + super( + new PayNowField(DOMAIN_ID, DOMAIN), + new PayNowField(PROXY_TYPE_ID, MOBILE_NUM_PROXY), + new PayNowField(MOBILE_NO_ID, SG_COUNTRY_CODE + phone), + new PayNowField(EDITABLE_ID, EDITABLE) + ); + } +} diff --git a/src/main/java/seedu/address/logic/paynow/PayNowCode.java b/src/main/java/seedu/address/logic/paynow/PayNowCode.java new file mode 100644 index 00000000000..ba3d859a418 --- /dev/null +++ b/src/main/java/seedu/address/logic/paynow/PayNowCode.java @@ -0,0 +1,93 @@ +package seedu.address.logic.paynow; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.stream.Stream; + +import com.google.zxing.WriterException; + +/** + * Represents a PayNow code that when encoded into a string and converted into a QR Code, + * can be scanned using a banking application to automatically fill in a person's + * mobile number and amount. + */ +public class PayNowCode extends PayNowPayload { + private static final int PAYLOAD_FORMAT_INDICATOR_ID = 0; + private static final int POINT_OF_INITIATION_METHOD_ID = 1; + private static final int MERCHANT_ACCOUNT_INFORMATION_ID = 26; + private static final int MERCHANT_CATEGORY_CODE_ID = 52; + private static final int TRANSACTION_CURRENCY_ID = 53; + private static final int TRANSACTION_AMOUNT_ID = 54; + private static final int COUNTRY_CODE_ID = 58; + private static final int MERCHANT_NAME_ID = 59; + private static final int MERCHANT_CITY_ID = 60; + private static final int CRC_ID = 63; + private static final int SG_CURRENCY_CODE = 702; + private static final String NA_MERCHANT_CATEGORY = "0000"; + private static final PayNowField PAYLOAD_FORMAT_INDICATOR = + new PayNowField(PAYLOAD_FORMAT_INDICATOR_ID, "01"); + private static final PayNowField POINT_OF_INITIATION_METHOD = + new PayNowField(POINT_OF_INITIATION_METHOD_ID, "11"); + private static final PayNowField MERCHANT_CATEGORY_CODE = + new PayNowField(MERCHANT_CATEGORY_CODE_ID, NA_MERCHANT_CATEGORY); + private static final PayNowField TRANSACTION_CURRENCY = + new PayNowField(TRANSACTION_CURRENCY_ID, SG_CURRENCY_CODE); + private static final PayNowField COUNTRY_CODE = + new PayNowField(COUNTRY_CODE_ID, "SG"); + private static final PayNowField MERCHANT_NAME = + new PayNowField(MERCHANT_NAME_ID, "NA"); + private static final PayNowField MERCHANT_CITY = + new PayNowField(MERCHANT_CITY_ID, "Singapore"); + private static final String PLACEHOLDER_CRC = "0000"; + + private PayNowCode(PayNowField... fields) { + super(fields); + } + + /** + * Generates a PayNow QR Code that users can scan with their banking apps which will + * automatically fill in the phone number and amount passed in as parameters. + */ + public static ByteArrayInputStream generatePayNowQrCode(String phone, float amount) + throws WriterException, IOException { + PayNowField[] fields = + new PayNowField[]{PAYLOAD_FORMAT_INDICATOR, + POINT_OF_INITIATION_METHOD, + new PayNowField( + MERCHANT_ACCOUNT_INFORMATION_ID, new MerchantAccountInformation(phone)), + MERCHANT_CATEGORY_CODE, + TRANSACTION_CURRENCY, + new PayNowField(TRANSACTION_AMOUNT_ID, amount), + COUNTRY_CODE, + MERCHANT_NAME, + MERCHANT_CITY, + new PayNowField(CRC_ID, PLACEHOLDER_CRC)}; + + String encodedFields = Stream.of(fields) + .map(PayNowField::toString) + .reduce("", (accumulator, encodedField) -> accumulator + encodedField); + + // Remove the placeholder CRC from the string + encodedFields = encodedFields.substring(0, encodedFields.length() - PLACEHOLDER_CRC.length()); + fields[fields.length - 1] = new PayNowField(CRC_ID, computeCrc(encodedFields)); + return QrGenerator.generateQrCode(new PayNowCode(fields).toString()); + } + + // @@author - zhekaiii-reused + // Reused from https://github.com/poonchuanan/Python-PayNow-QR-Code-Generator/blob/main/generatePayNowQR.py + // with minor modifications + private static String computeCrc(String payload) { + int crc = 0xFFFF; + int msb = crc >> 8; + int lsb = crc & 255; + for (char character : payload.toCharArray()) { + int x = character ^ msb; + x ^= (x >> 4); + msb = (lsb ^ (x >> 3) ^ (x << 4)) & 255; + lsb = (x ^ (x << 5)) & 255; + } + crc = (msb << 8) + lsb; + return String.format("%04X", crc); + } + // @@author +} diff --git a/src/main/java/seedu/address/logic/paynow/PayNowField.java b/src/main/java/seedu/address/logic/paynow/PayNowField.java new file mode 100644 index 00000000000..a15bd6a10ad --- /dev/null +++ b/src/main/java/seedu/address/logic/paynow/PayNowField.java @@ -0,0 +1,30 @@ +package seedu.address.logic.paynow; + +/** + * Represents a field within a {@code PaynowCode}. + */ +public final class PayNowField { + private final int id; + private final Object value; + + /** + * Returns a {@code PaynowField} with the id and the valye passed into the constructor. + */ + public PayNowField(int id, Object value) { + this.id = id; + this.value = value; + } + + @Override + public String toString() { + String valueString = value.toString(); + if (value instanceof Float) { + valueString = String.format("%.2f", value); + } else if (value instanceof Double) { + valueString = String.format("%.2f", value); + } + return String.format("%02d", id) + + String.format("%02d", valueString.length()) + + valueString; + } +} diff --git a/src/main/java/seedu/address/logic/paynow/PayNowPayload.java b/src/main/java/seedu/address/logic/paynow/PayNowPayload.java new file mode 100644 index 00000000000..c61c8c32c30 --- /dev/null +++ b/src/main/java/seedu/address/logic/paynow/PayNowPayload.java @@ -0,0 +1,23 @@ +package seedu.address.logic.paynow; + +/** + * This class represents the string information to be encoded into a QR code + * such that users can scan with their banking applications and transfer money via PayNow. + */ +public abstract class PayNowPayload { + private final PayNowField[] fields; + + protected PayNowPayload(PayNowField... fields) { + this.fields = new PayNowField[fields.length]; + System.arraycopy(fields, 0, this.fields, 0, fields.length); + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + for (PayNowField field : fields) { + stringBuilder.append(field.toString()); + } + return stringBuilder.toString(); + } +} diff --git a/src/main/java/seedu/address/logic/paynow/QrGenerator.java b/src/main/java/seedu/address/logic/paynow/QrGenerator.java new file mode 100644 index 00000000000..70166d0897a --- /dev/null +++ b/src/main/java/seedu/address/logic/paynow/QrGenerator.java @@ -0,0 +1,88 @@ +package seedu.address.logic.paynow; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Hashtable; +import javax.imageio.ImageIO; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +/** + * This helper class helps to generate a QR Code image given a string. + */ +public class QrGenerator { + private static final int QR_SIZE = 400; + private static final int LOGO_HEIGHT = QR_SIZE / 6; + private static final Color PAYNOW_COLOR = Color.decode("#7a1b78"); + private static final String LOGO_PATH = "images/paynowlogo.png"; + + private static void overlayLogo(Graphics2D graphics) throws IOException { + Image logo = getLogo(); + int startHeight = (QR_SIZE - logo.getHeight(null)) / 2; + int startWidth = (QR_SIZE - logo.getWidth(null)) / 2; + graphics.drawImage(logo, startWidth, startHeight, null); + } + + private static Image getLogo() throws IOException { + BufferedImage originalLogo = ImageIO.read(ClassLoader.getSystemResourceAsStream(LOGO_PATH)); + float scale = (float) LOGO_HEIGHT / originalLogo.getHeight(); + return originalLogo.getScaledInstance( + (int) (originalLogo.getWidth() * scale), + (int) (originalLogo.getHeight() * scale), + Image.SCALE_SMOOTH); + } + + private static BitMatrix encodeText(String qrCodeText) throws WriterException { + // Create the ByteMatrix for the QR-Code that encodes the given String + Hashtable hintMap = new Hashtable<>(); + hintMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); + QRCodeWriter qrCodeWriter = new QRCodeWriter(); + return qrCodeWriter.encode(qrCodeText, BarcodeFormat.QR_CODE, QR_SIZE, QR_SIZE, hintMap); + } + + private static BufferedImage fillImageFromBitMatrix(BitMatrix bitMatrix) { + // Make the BufferedImage that are to hold the QRCode + int matrixWidth = bitMatrix.getWidth(); + int matrixHeight = bitMatrix.getHeight(); + BufferedImage image = new BufferedImage(matrixWidth, matrixHeight, BufferedImage.TYPE_INT_RGB); + image.createGraphics(); + + Graphics2D graphics = (Graphics2D) image.getGraphics(); + graphics.setColor(Color.WHITE); + graphics.fillRect(0, 0, matrixWidth, matrixHeight); + // Paint and save the image using the ByteMatrix + graphics.setColor(PAYNOW_COLOR); + + for (int i = 0; i < matrixWidth; i++) { + for (int j = 0; j < matrixWidth; j++) { + if (bitMatrix.get(i, j)) { + graphics.fillRect(i, j, 1, 1); + } + } + } + return image; + } + + + /** + * Converts the given text into a QR Code and returns the Image. + */ + public static ByteArrayInputStream generateQrCode(String qrCodeText) throws WriterException, IOException { + BitMatrix bitMatrix = encodeText(qrCodeText); + BufferedImage image = fillImageFromBitMatrix(bitMatrix); + overlayLogo((Graphics2D) image.getGraphics()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + ImageIO.write(image, "jpeg", os); + return new ByteArrayInputStream(os.toByteArray()); + } +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..cc9d448b6ba 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,6 +1,8 @@ package seedu.address.model; import java.nio.file.Path; +import java.util.Comparator; +import java.util.Optional; import java.util.function.Predicate; import javafx.collections.ObservableList; @@ -11,19 +13,22 @@ * The API of the Model component. */ public interface Model { - /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; - /** - * Replaces user prefs data with the data in {@code userPrefs}. + * {@code Predicate} that always evaluate to true */ - void setUserPrefs(ReadOnlyUserPrefs userPrefs); + Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + int INVALID_PERSON_INDEX = -1; /** * Returns the user prefs. */ ReadOnlyUserPrefs getUserPrefs(); + /** + * Replaces user prefs data with the data in {@code userPrefs}. + */ + void setUserPrefs(ReadOnlyUserPrefs userPrefs); + /** * Returns the user prefs' GUI settings. */ @@ -44,14 +49,16 @@ public interface Model { */ void setAddressBookFilePath(Path addressBookFilePath); + /** + * Returns the AddressBook + */ + ReadOnlyAddressBook getAddressBook(); + /** * Replaces address book data with the data in {@code addressBook}. */ void setAddressBook(ReadOnlyAddressBook addressBook); - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); - /** * Returns true if a person with the same identity as {@code person} exists in the address book. */ @@ -76,12 +83,23 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); - /** Returns an unmodifiable view of the filtered person list */ + /** + * Returns an unmodifiable view of the filtered person list + */ ObservableList getFilteredPersonList(); + ObservableList getSortedPersonList(); + /** * Updates the filter of the filtered person list to filter by the given {@code predicate}. + * * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + void updatePersonComparator(Comparator personComparator); + + Optional findPerson(Predicate predicate); + + int findIndex(Person person); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..b36633f2b4f 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,11 +4,14 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Comparator; +import java.util.Optional; import java.util.function.Predicate; import java.util.logging.Logger; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; import seedu.address.model.person.Person; @@ -22,6 +25,7 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final SortedList sortedPersons; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -33,7 +37,8 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + sortedPersons = new SortedList<>(this.addressBook.getPersonList()); + filteredPersons = new FilteredList<>(sortedPersons); } public ModelManager() { @@ -43,14 +48,14 @@ public ModelManager() { //=========== UserPrefs ================================================================================== @Override - public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { - requireNonNull(userPrefs); - this.userPrefs.resetData(userPrefs); + public ReadOnlyUserPrefs getUserPrefs() { + return userPrefs; } @Override - public ReadOnlyUserPrefs getUserPrefs() { - return userPrefs; + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + requireNonNull(userPrefs); + this.userPrefs.resetData(userPrefs); } @Override @@ -78,13 +83,13 @@ public void setAddressBookFilePath(Path addressBookFilePath) { //=========== AddressBook ================================================================================ @Override - public void setAddressBook(ReadOnlyAddressBook addressBook) { - this.addressBook.resetData(addressBook); + public ReadOnlyAddressBook getAddressBook() { + return addressBook; } @Override - public ReadOnlyAddressBook getAddressBook() { - return addressBook; + public void setAddressBook(ReadOnlyAddressBook addressBook) { + this.addressBook.resetData(addressBook); } @Override @@ -122,12 +127,35 @@ public ObservableList getFilteredPersonList() { return filteredPersons; } + @Override + public ObservableList getSortedPersonList() { + return sortedPersons; + } + @Override public void updateFilteredPersonList(Predicate predicate) { requireNonNull(predicate); filteredPersons.setPredicate(predicate); } + @Override + public void updatePersonComparator(Comparator personComparator) { + sortedPersons.setComparator(personComparator); + } + + @Override + public Optional findPerson(Predicate predicate) { + return addressBook.getPersonList() + .stream() + .filter(predicate) + .findAny(); + } + + @Override + public int findIndex(Person person) { + return filteredPersons.indexOf(person); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -142,7 +170,7 @@ public boolean equals(Object other) { ModelManager otherModelManager = (ModelManager) other; return addressBook.equals(otherModelManager.addressBook) && userPrefs.equals(otherModelManager.userPrefs) - && filteredPersons.equals(otherModelManager.filteredPersons); + && filteredPersons.equals(otherModelManager.filteredPersons) + && sortedPersons.equals(otherModelManager.sortedPersons); } - } diff --git a/src/main/java/seedu/address/model/person/Birthday.java b/src/main/java/seedu/address/model/person/Birthday.java new file mode 100644 index 00000000000..ffa95601767 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Birthday.java @@ -0,0 +1,105 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Comparator; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents a Person's birthday in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidBirthday(String)} + */ +public class Birthday { + public static final String BIRTHDAY_CONSTRAINTS = + "Birthday should be a valid date, in the format dd/mm/yyyy and be before today"; + public static final String BIRTHDAY_FORMAT = "dd/MM/yyyy"; + + /** + * This comparator will sort contacts with no birthdays to the back. + * Contacts with their next birthday closest to today will be put first. + */ + public static final Comparator BIRTHDAY_COMPARATOR = (personA, personB) -> { + if (personA.getBirthday().birthday == null) { + return 1; + } + if (personB.getBirthday().birthday == null) { + return -1; + } + LocalDate now = LocalDate.now(); + LocalDate nextABirthday = personA.getBirthday().birthday.withYear(now.getYear()); + if (nextABirthday.isBefore(now)) { + nextABirthday = nextABirthday.plusYears(1); + } + LocalDate nextBBirthday = personB.getBirthday().birthday.withYear(now.getYear()); + if (nextBBirthday.isBefore(now)) { + nextBBirthday = nextBBirthday.plusYears(1); + } + return nextABirthday.compareTo(nextBBirthday); + }; + + public final LocalDate birthday; + + + /** + * Constructs a {@code Birthday}. + * + * @param birthday A valid birthday, or an empty string. + */ + public Birthday(String birthday) { + requireNonNull(birthday); + if (birthday.isBlank()) { + this.birthday = null; + return; + } + checkArgument(isValidBirthday(birthday), BIRTHDAY_CONSTRAINTS); + this.birthday = LocalDate.parse(birthday, DateTimeFormatter.ofPattern(BIRTHDAY_FORMAT)); + } + + + /** + * Returns true if a given string is a valid birthday. + */ + public static boolean isValidBirthday(String test) { + if (test == null || test.isBlank()) { + return true; + } + test = test.strip(); + try { + LocalDate date = LocalDate.parse(test, DateTimeFormatter.ofPattern(BIRTHDAY_FORMAT)); + return date.format(DateTimeFormatter.ofPattern(BIRTHDAY_FORMAT)).equals(test) + && date.isBefore(LocalDate.now()); + } catch (DateTimeParseException e) { + return false; + } + } + + @Override + public String toString() { + return Optional.ofNullable(birthday).map( + birthdayObj -> birthdayObj.format(DateTimeFormatter.ofPattern( + BIRTHDAY_FORMAT + ))).orElse(""); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Birthday)) { + return false; + } + Birthday otherBirthday = (Birthday) o; + return Objects.equals(birthday, otherBirthday.birthday); + } + + @Override + public int hashCode() { + return Objects.hash(birthday); + } +} diff --git a/src/main/java/seedu/address/model/person/Day.java b/src/main/java/seedu/address/model/person/Day.java new file mode 100644 index 00000000000..4f5b5020136 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Day.java @@ -0,0 +1,65 @@ +package seedu.address.model.person; + +import java.util.Arrays; + +/** + * Day enumeration models the 7 days of the week within FriendFolio. + * Since days of the week are distinct, a set of available days is + * easily represented by a HashSet containing some subset of all possible + * Day. + */ +public enum Day { + SUNDAY, + MONDAY, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY; + + /** + * Checks whether a String input matches the name of one of the days defined in the + * Day enum. Matching is not case-sensitive. This method should always be used before + * the getDay function to avoid null-handling requirements. isValidDay is decoupled from + * getDay only in tests. + * @param day + * @return boolean representing if a match is found. + */ + public static final String MESSAGE_CONSTRAINTS = "Please enter a valid day of the week from the following: " + + getAllDaysAsString(); + + public static boolean isValidDay(String day) { + return Arrays.stream(Day.values()).anyMatch(x -> x.toString().equalsIgnoreCase(day)); + } + + /** + * Maps a String input to one of the days defined in the Day enum. Matching is not + * case-sensitive. While the output is null if no match, isValidDay method should + * be used to check if the string is valid first. getDay is decoupled from isValidDay + * only in tests. + * @param day + * @return Day whose name matches the String argument passed, or null if no match is found + */ + public static Day getDay(String day) { + return Arrays.stream(Day.values()) + .filter(x -> x.toString().equalsIgnoreCase(day)) + .reduce((x, y) -> x) + .orElse(null); + } + + /** + * Helper function to return string containing all days in Day enum. + * @return String containing all days' names in Day enum, split by comma. + */ + private static String getAllDaysAsString() { + StringBuilder s = new StringBuilder(); + for (Day d: Day.values()) { + s.append(d).append(", "); + } + return s.toString().substring(0, s.length() - 2); + } + + public String getShortForm() { + return this.name().substring(0, 3); + } +} diff --git a/src/main/java/seedu/address/model/person/MoneyOwed.java b/src/main/java/seedu/address/model/person/MoneyOwed.java new file mode 100644 index 00000000000..b4ffad58c35 --- /dev/null +++ b/src/main/java/seedu/address/model/person/MoneyOwed.java @@ -0,0 +1,144 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.util.Comparator; + +/** + * Represents a Person's money owed in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidMoney(String)} + */ +public class MoneyOwed { + + public static final String MESSAGE_CONSTRAINTS = + "Money Owed should be at most 2 decimal places in the following format 'xxx.xx' or '-xxx.xx'.\n" + + "Total amount you owe or other owe you should not be more than $100,000."; + public static final String VALIDATION_REGEX = "^(?:-)?\\d+(\\.\\d{0,2})?"; + + public static final String NO_MONEY_OWED_MESSAGE = "You don't owe each other anything"; + public static final String USER_OWES_MONEY_MESSAGE = "You owe $%s"; + public static final String PERSON_OWES_MONEY_MESSAGE = "Owes you $%s"; + public static final Float MAXIMUM_AMOUNT = (float) 100000; + public static final Float MINIMUM_AMOUNT = (float) -100000; + + /** + * This comparator will sort contacts with no money owed to the back. + * Contacts that the user owes the most money to will be put first. + * Contacts who owes the most money will be put right after contacts that + * the user owes money to. + */ + public static final Comparator MONEY_COMPARATOR = (personA, personB) -> { + // If user owes personA money means personA.getMoneyOwed().moneyOwed < 0. So sort in asc order. + if (personA.getMoneyOwed().userOwesMoney()) { + return Float.compare(personA.getMoneyOwed().moneyOwed, personB.getMoneyOwed().moneyOwed); + } + // personB moneyOwed < 0 but personA moneyOwed >= 0. Put personB before personA. + if (personB.getMoneyOwed().userOwesMoney()) { + return 1; + } + // Both personA and personB >= 0. Put the larger one first. + return Float.compare(personB.getMoneyOwed().moneyOwed, personA.getMoneyOwed().moneyOwed); + }; + + public final Float moneyOwed; + + /** + * Constructs a {@code MoneyOwed}. + * + * @param money A valid amount of money owed. + */ + public MoneyOwed(String money) { + requireNonNull(money); + checkArgument(isValidMoney(money), MESSAGE_CONSTRAINTS); + moneyOwed = Float.parseFloat(money); + } + + /** + * Returns true if a given string is a valid money amount. + */ + public static boolean isValidMoney(String test) { + if (test == null) { + return true; + } + Float value; + try { + value = Float.parseFloat(test); + } catch (NumberFormatException e) { + return false; + } + return test.matches(VALIDATION_REGEX) + && value >= MINIMUM_AMOUNT + && value <= MAXIMUM_AMOUNT; + } + + /** + * Returns true if a moneyOwed is negative. + */ + public boolean userOwesMoney() { + return (moneyOwed < 0); + } + + /** + * Returns the amount of money owed. + */ + public Float getAmount() { + return this.moneyOwed; + } + + /** + * Returns the absolute amount of money owed. + */ + public Float getAbsoluteAmount() { + return Math.abs(this.moneyOwed); + } + + + /** + * Returns a MoneyOwed object with the new amount owed. + */ + public MoneyOwed addAmountOwed(Float addedAmount) throws IllegalArgumentException { + String replacedString = String.valueOf(moneyOwed + addedAmount); + checkArgument(isValidMoney(replacedString), MESSAGE_CONSTRAINTS); + return new MoneyOwed(replacedString); + } + + /** + * Returns message to display on UI in String. + */ + public String getMessage() { + if (moneyOwed == 0) { + return NO_MONEY_OWED_MESSAGE; + } + if (userOwesMoney()) { + return String.format(USER_OWES_MONEY_MESSAGE, toString().substring(1)); + } else { + return String.format(PERSON_OWES_MONEY_MESSAGE, this); + } + } + + @Override + public String toString() { + return String.format("%.2f", moneyOwed); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof MoneyOwed)) { + return false; + } + + MoneyOwed otherName = (MoneyOwed) other; + return moneyOwed.equals(otherName.moneyOwed); + } + + @Override + public int hashCode() { + return moneyOwed.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..0e2a52806d8 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -3,6 +3,8 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import java.util.Comparator; + /** * Represents a Person's name in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} @@ -18,6 +20,9 @@ public class Name { */ public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final Comparator NAME_COMPARATOR = (personA, personB) -> + personA.getName().fullName.compareToIgnoreCase(personB.getName().fullName); + public final String fullName; /** diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..24b7d56d9a3 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -23,18 +23,27 @@ public class Person { // Data fields private final Address address; + private final Remark remark; private final Set tags = new HashSet<>(); + private final Birthday birthday; + private final MoneyOwed moneyOwed; + private final Set daysAvailable = new HashSet<>(); /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { + public Person(Name name, Phone phone, Email email, Address address, + Remark remark, Set tags, Birthday birthday, MoneyOwed moneyOwed, Set daysAvailable) { requireAllNonNull(name, phone, email, address, tags); this.name = name; this.phone = phone; this.email = email; this.address = address; + this.remark = remark; this.tags.addAll(tags); + this.birthday = birthday; + this.moneyOwed = moneyOwed; + this.daysAvailable.addAll(daysAvailable); } public Name getName() { @@ -49,10 +58,18 @@ public Email getEmail() { return email; } + public MoneyOwed getMoneyOwed() { + return moneyOwed; + } + public Address getAddress() { return address; } + public Remark getRemark() { + return remark; + } + /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -61,6 +78,14 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + public Birthday getBirthday() { + return birthday; + } + + public Set getDaysAvailable() { + return Collections.unmodifiableSet(daysAvailable); + } + /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. @@ -94,13 +119,16 @@ public boolean equals(Object other) { && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) - && tags.equals(otherPerson.tags); + && tags.equals(otherPerson.tags) + && birthday.equals(otherPerson.birthday) + && moneyOwed.equals(otherPerson.moneyOwed) + && daysAvailable.equals(otherPerson.daysAvailable); } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, address, tags, birthday, moneyOwed, daysAvailable); } @Override @@ -110,7 +138,11 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("remark", remark) .add("tags", tags) + .add("birthday", birthday) + .add("moneyOwed", moneyOwed) + .add("daysAvailable", daysAvailable) .toString(); } diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d733f63d739..4992873c9d1 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -33,6 +33,14 @@ public static boolean isValidPhone(String test) { return test.matches(VALIDATION_REGEX); } + /** + * Returns true if the phone number is a valid Singaporean number, i.e. 8 digits starting + * with an 8 or a 9. + */ + public boolean isSingaporeanNumber() { + return (this.value.startsWith("8") || this.value.startsWith("9")) && this.value.length() == 8; + } + @Override public String toString() { return value; diff --git a/src/main/java/seedu/address/model/person/Remark.java b/src/main/java/seedu/address/model/person/Remark.java new file mode 100644 index 00000000000..b0e3cc7bb9e --- /dev/null +++ b/src/main/java/seedu/address/model/person/Remark.java @@ -0,0 +1,36 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +/** + * Represents a Person's remark in the address book. + * Guarantees: immutable; is always valid + */ +public class Remark { + public final String value; + + /** + * @param remark string representing the remark of a contact + */ + public Remark(String remark) { + requireNonNull(remark); + value = remark; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Remark // instanceof handles nulls + && value.equals(((Remark) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/exceptions/InvalidSortTypeException.java b/src/main/java/seedu/address/model/person/exceptions/InvalidSortTypeException.java new file mode 100644 index 00000000000..6710938bf3a --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/InvalidSortTypeException.java @@ -0,0 +1,14 @@ +package seedu.address.model.person.exceptions; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_SORT_TYPE; + +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Signals that the user has typed in an invalid sort method. + */ +public class InvalidSortTypeException extends ParseException { + public InvalidSortTypeException(String type) { + super(String.format(MESSAGE_INVALID_SORT_TYPE, type)); + } +} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicate.java similarity index 52% rename from src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java rename to src/main/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicate.java index 62d19be2977..74e807fe003 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicate.java @@ -1,25 +1,41 @@ -package seedu.address.model.person; +package seedu.address.model.person.predicates; import java.util.List; import java.util.function.Predicate; import seedu.address.commons.util.StringUtil; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; /** * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. */ public class NameContainsKeywordsPredicate implements Predicate { private final List keywords; + private final boolean matchAll; - public NameContainsKeywordsPredicate(List keywords) { + /** + * Constructs a NameContainsKeywordsPredicate with the given keywords and a boolean + * flag to indicate if we should match all or any. + */ + public NameContainsKeywordsPredicate(List keywords, boolean matchAll) { this.keywords = keywords; + this.matchAll = matchAll; + } + + public NameContainsKeywordsPredicate(List keywords) { + this(keywords, false); } @Override public boolean test(Person person) { + Predicate predicate = keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword); + if (matchAll) { + return keywords.stream() + .allMatch(predicate); + } return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + .anyMatch(predicate); } @Override @@ -34,11 +50,15 @@ public boolean equals(Object other) { } NameContainsKeywordsPredicate otherNameContainsKeywordsPredicate = (NameContainsKeywordsPredicate) other; - return keywords.equals(otherNameContainsKeywordsPredicate.keywords); + return keywords.equals(otherNameContainsKeywordsPredicate.keywords) + && matchAll == otherNameContainsKeywordsPredicate.matchAll; } @Override public String toString() { - return new ToStringBuilder(this).add("keywords", keywords).toString(); + return new ToStringBuilder(this) + .add("keywords", keywords) + .add("matchAll", matchAll) + .toString(); } } diff --git a/src/main/java/seedu/address/model/person/predicates/PersonAvailableOnDayPredicate.java b/src/main/java/seedu/address/model/person/predicates/PersonAvailableOnDayPredicate.java new file mode 100644 index 00000000000..b03a488f378 --- /dev/null +++ b/src/main/java/seedu/address/model/person/predicates/PersonAvailableOnDayPredicate.java @@ -0,0 +1,64 @@ +package seedu.address.model.person.predicates; + +import java.util.Collection; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Day; +import seedu.address.model.person.Person; + +/** + * Tests that a {@code Person}'s {@code daysAvailable} matches any of the days given. + */ +public class PersonAvailableOnDayPredicate implements Predicate { + private final Collection daysAvailable; + private final boolean matchAll; + + /** + * Constructs a PersonAvailableOnDayPredicate with the given keywords and a boolean + * flag to indicate if we should match all or any. + */ + public PersonAvailableOnDayPredicate(Collection daysAvailable, boolean matchAll) { + this.daysAvailable = daysAvailable; + this.matchAll = matchAll; + } + + public PersonAvailableOnDayPredicate(Collection daysAvailable) { + this(daysAvailable, false); + } + + @Override + public boolean test(Person person) { + Predicate predicate = day -> person.getDaysAvailable().contains(day); + if (matchAll) { + return daysAvailable.stream() + .allMatch(predicate); + } + return daysAvailable.stream() + .anyMatch(predicate); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PersonAvailableOnDayPredicate)) { + return false; + } + + PersonAvailableOnDayPredicate otherPersonAvailableOnDayPredicate = (PersonAvailableOnDayPredicate) other; + return daysAvailable.equals(otherPersonAvailableOnDayPredicate.daysAvailable) + && matchAll == otherPersonAvailableOnDayPredicate.matchAll; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("keywords", daysAvailable) + .add("matchAll", matchAll) + .toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/predicates/PersonHasTagPredicate.java b/src/main/java/seedu/address/model/person/predicates/PersonHasTagPredicate.java new file mode 100644 index 00000000000..6bc6a60f154 --- /dev/null +++ b/src/main/java/seedu/address/model/person/predicates/PersonHasTagPredicate.java @@ -0,0 +1,65 @@ +package seedu.address.model.person.predicates; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Tests that a {@code Person}'s {@code Tag}s matches any of the keywords given. + */ +public class PersonHasTagPredicate implements Predicate { + + private final List keywords; + private final boolean matchAll; + + /** + * Constructs a PersonHasTagPredicate with the given keywords and a boolean + * flag to indicate if we should match all or any. + */ + public PersonHasTagPredicate(List keywords, boolean matchAll) { + this.keywords = keywords; + this.matchAll = matchAll; + } + + public PersonHasTagPredicate(List keywords) { + this(keywords, false); + } + + @Override + public boolean test(Person person) { + Predicate predicate = keyword -> person.getTags().contains(keyword); + if (matchAll) { + return keywords.stream() + .allMatch(predicate); + } + return keywords.stream() + .anyMatch(predicate); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PersonHasTagPredicate)) { + return false; + } + + PersonHasTagPredicate otherPersonHasTagPredicate = (PersonHasTagPredicate) other; + return keywords.equals(otherPersonHasTagPredicate.keywords) + && matchAll == otherPersonHasTagPredicate.matchAll; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("keywords", keywords) + .add("matchAll", matchAll) + .toString(); + } +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index f1a0d4e233b..4062867acfa 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -44,12 +44,12 @@ public boolean equals(Object other) { } Tag otherTag = (Tag) other; - return tagName.equals(otherTag.tagName); + return tagName.equalsIgnoreCase(otherTag.tagName); } @Override public int hashCode() { - return tagName.hashCode(); + return tagName.toLowerCase().hashCode(); } /** diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..109dd41c40b 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -7,36 +7,47 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; +import seedu.address.model.person.Day; import seedu.address.model.person.Email; +import seedu.address.model.person.MoneyOwed; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Remark; import seedu.address.model.tag.Tag; /** * Contains utility methods for populating {@code AddressBook} with sample data. */ public class SampleDataUtil { + + public static final Remark EMPTY_REMARK = new Remark(""); public static Person[] getSamplePersons() { return new Person[] { new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), + new Address("Blk 30 Geylang Street 29, #06-40"), EMPTY_REMARK, getTagSet("friends"), + new Birthday(""), new MoneyOwed("0"), getDaysAvailableSet("tuesday", "monday")), new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), EMPTY_REMARK, + getTagSet("colleagues", "friends"), new Birthday(""), new MoneyOwed("0"), + getDaysAvailableSet("monday")), new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), + new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), EMPTY_REMARK, + getTagSet("neighbours"), new Birthday(""), new MoneyOwed("0"), + getDaysAvailableSet("monday")), new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), EMPTY_REMARK, + getTagSet("family"), new Birthday(""), new MoneyOwed("0"), + getDaysAvailableSet("monday")), new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), + new Address("Blk 47 Tampines Street 20, #17-35"), EMPTY_REMARK, + getTagSet("classmates"), new Birthday(""), new MoneyOwed("0"), + getDaysAvailableSet("monday")), new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + new Address("Blk 45 Aljunied Street 85, #11-31"), EMPTY_REMARK, + getTagSet("colleagues"), new Birthday(""), new MoneyOwed("0"), + getDaysAvailableSet("monday")), }; } @@ -57,4 +68,10 @@ public static Set getTagSet(String... strings) { .collect(Collectors.toSet()); } + private static Set getDaysAvailableSet(String... strings) { + return Arrays.stream(strings) + .map(Day::getDay) + .collect(Collectors.toSet()); + } + } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..8abee65ad8f 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -11,10 +12,14 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.person.Address; +import seedu.address.model.person.Birthday; +import seedu.address.model.person.Day; import seedu.address.model.person.Email; +import seedu.address.model.person.MoneyOwed; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Remark; import seedu.address.model.tag.Tag; /** @@ -28,22 +33,36 @@ class JsonAdaptedPerson { private final String phone; private final String email; private final String address; + private final String remark; private final List tags = new ArrayList<>(); + private final String birthday; + private final String moneyOwed; + private final Set daysAvailable = new HashSet<>(); /** * Constructs a {@code JsonAdaptedPerson} with the given person details. */ @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("email") String email, @JsonProperty("address") String address, + @JsonProperty("remark") String remark, + @JsonProperty("tags") List tags, + @JsonProperty("birthday") String birthday, + @JsonProperty("moneyOwed") String moneyOwed, + @JsonProperty("daysAvailable") Set daysAvailable) { this.name = name; this.phone = phone; this.email = email; this.address = address; + this.remark = remark; if (tags != null) { this.tags.addAll(tags); } + this.birthday = birthday; + this.moneyOwed = moneyOwed; + if (daysAvailable != null) { + this.daysAvailable.addAll(daysAvailable); + } } /** @@ -54,9 +73,13 @@ public JsonAdaptedPerson(Person source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; + remark = source.getRemark().value; tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + birthday = source.getBirthday().toString(); + moneyOwed = source.getMoneyOwed().toString(); + daysAvailable.addAll(source.getDaysAvailable()); } /** @@ -64,8 +87,10 @@ public JsonAdaptedPerson(Person source) { * * @throws IllegalValueException if there were any data constraints violated in the adapted person. */ + @SuppressWarnings("checkstyle:Regexp") public Person toModelType() throws IllegalValueException { final List personTags = new ArrayList<>(); + for (JsonAdaptedTag tag : tags) { personTags.add(tag.toModelType()); } @@ -102,8 +127,23 @@ public Person toModelType() throws IllegalValueException { } final Address modelAddress = new Address(address); + final Remark modelRemark = new Remark(Optional.ofNullable(remark).orElse("")); + final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + + if (!Birthday.isValidBirthday(birthday)) { + throw new IllegalValueException(Birthday.BIRTHDAY_CONSTRAINTS); + } + final Birthday modelBirthday = new Birthday(Optional.ofNullable(birthday).orElse("")); + if (!MoneyOwed.isValidMoney(moneyOwed)) { + throw new IllegalValueException(MoneyOwed.MESSAGE_CONSTRAINTS); + } + final MoneyOwed modelMoneyOwed = new MoneyOwed(Optional.ofNullable(moneyOwed).orElse("0")); + + final Set modelDaysAvailable = new HashSet<>(daysAvailable); + + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelRemark, + modelTags, modelBirthday, modelMoneyOwed, modelDaysAvailable); } } diff --git a/src/main/java/seedu/address/ui/DisplayCard.java b/src/main/java/seedu/address/ui/DisplayCard.java new file mode 100644 index 00000000000..638c889d6b5 --- /dev/null +++ b/src/main/java/seedu/address/ui/DisplayCard.java @@ -0,0 +1,128 @@ +package seedu.address.ui; + +import java.util.Comparator; + +import javafx.animation.FadeTransition; +import javafx.animation.TranslateTransition; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.Region; +import seedu.address.commons.util.AnimationUtil; +import seedu.address.model.person.Person; + +/** + * Displays a person's information + */ +public class DisplayCard extends UiPart { + + private static final String FXML = "DisplayCard.fxml"; + private static final int IMAGE_SIZE = 30; + + public final Person person; + private final Image tagIconImage = new Image( + this.getClass().getResourceAsStream("/images/tag_icon.png"), + IMAGE_SIZE, IMAGE_SIZE, true, true); + private final Image dayIconImage = new Image( + this.getClass().getResourceAsStream("/images/day_icon.png"), + IMAGE_SIZE, IMAGE_SIZE, true, true); + private final Image phoneIconImage = new Image( + this.getClass().getResourceAsStream("/images/phone_icon.png"), + IMAGE_SIZE, IMAGE_SIZE, true, true); + private final Image addressIconImage = new Image( + this.getClass().getResourceAsStream("/images/address_icon.png"), + IMAGE_SIZE, IMAGE_SIZE, true, true); + private final Image emailIconImage = new Image( + this.getClass().getResourceAsStream("/images/email_icon.png"), + IMAGE_SIZE, IMAGE_SIZE, true, true); + private final Image birthdayIconImage = new Image( + this.getClass().getResourceAsStream("/images/birthday_icon.png"), + IMAGE_SIZE, IMAGE_SIZE, true, true); + private final Image moneyIconImage = new Image( + this.getClass().getResourceAsStream("/images/money_icon.png"), + IMAGE_SIZE, IMAGE_SIZE, true, true); + private final TranslateTransition moveTransition = AnimationUtil.getMoveTransition(getRoot()); + private final TranslateTransition bounceBackTransition = AnimationUtil.getBounceBackTransition(getRoot()); + private final FadeTransition fadeInTransition = AnimationUtil.getFadeInTransition(getRoot()); + @FXML + private Label name; + @FXML + private Label phone; + @FXML + private Label address; + @FXML + private Label email; + @FXML + private FlowPane tags; + @FXML + private FlowPane daysAvailable; + @FXML + private Label birthday; + @FXML + private Label remark; + @FXML + private Label moneyOwed; + @FXML + private ImageView tagIcon; + @FXML + private ImageView dayIcon; + @FXML + private ImageView phoneIcon; + @FXML + private ImageView addressIcon; + @FXML + private ImageView emailIcon; + @FXML + private ImageView birthdayIcon; + @FXML + private ImageView moneyIcon; + + /** + * @param person Person information to be displayed on the card + */ + public DisplayCard(Person person) { + super(FXML); + + this.person = person; + setUpLabels(person); + setUpIcons(); + playAnimation(); + } + + private void setUpLabels(Person person) { + name.setText(person.getName().fullName); + phone.setText(person.getPhone().value); + address.setText(person.getAddress().value); + email.setText(person.getEmail().value); + remark.setText(person.getRemark().value); + person.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + person.getDaysAvailable().stream() + .sorted(Comparator.comparing(Enum::ordinal)) + .forEach(day -> daysAvailable.getChildren().add(new Label(day.getShortForm()))); + birthday.setText(person.getBirthday().toString()); + moneyOwed.setText(person.getMoneyOwed().getMessage()); + } + + private void setUpIcons() { + tagIcon.setImage(tagIconImage); + dayIcon.setImage(dayIconImage); + phoneIcon.setImage(phoneIconImage); + addressIcon.setImage(addressIconImage); + emailIcon.setImage(emailIconImage); + birthdayIcon.setImage(birthdayIconImage); + moneyIcon.setImage(moneyIconImage); + } + + /** + * Plays the card's animations + */ + public void playAnimation() { + fadeInTransition.playFromStart(); + moveTransition.playFromStart(); + bounceBackTransition.playFromStart(); + } +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..3e4f4dbc5f3 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2324s2-cs2103t-t16-2.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); @@ -46,21 +46,21 @@ public HelpWindow() { /** * Shows the help window. - * @throws IllegalStateException - *
    - *
  • - * if this method is called on a thread other than the JavaFX Application Thread. - *
  • - *
  • - * if this method is called during animation or layout processing. - *
  • - *
  • - * if this method is called on the primary stage. - *
  • - *
  • - * if {@code dialogStage} is already showing. - *
  • - *
+ * + * @throws IllegalStateException
    + *
  • + * if this method is called on a thread other than the JavaFX Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
*/ public void show() { logger.fine("Showing help page about the application."); diff --git a/src/main/java/seedu/address/ui/HomeCard.java b/src/main/java/seedu/address/ui/HomeCard.java new file mode 100644 index 00000000000..1b560cefa90 --- /dev/null +++ b/src/main/java/seedu/address/ui/HomeCard.java @@ -0,0 +1,171 @@ +package seedu.address.ui; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import javafx.animation.FadeTransition; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.animation.TranslateTransition; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.chart.BarChart; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import javafx.util.Duration; +import seedu.address.commons.util.AnimationUtil; +import seedu.address.model.person.Day; +import seedu.address.model.person.Person; + +/** + * A UI component that displays information of a {@code Person}. + */ +public class HomeCard extends UiPart { + + private static final String FXML = "HomeCard.fxml"; + private static final String MONEY_OWED_LABEL = "Money you owe"; + private static final String OWED_MONEY_LABEL = "Money owed to you"; + private static final double CATEGORY_GAP = 120; + private static final double BAR_GAP = 0; + private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); + private final DateTimeFormatter secondFormatter = DateTimeFormatter.ofPattern(":ss"); + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("EEEE, MMMM d"); + private final ObservableList personList; + private final TranslateTransition moveTransition = AnimationUtil.getMoveTransition(getRoot()); + private final TranslateTransition bounceBackTransition = AnimationUtil.getBounceBackTransition(getRoot()); + private final FadeTransition fadeInTransition = AnimationUtil.getFadeInTransition(getRoot()); + + /** + * 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 + */ + + @FXML + private Label time; + @FXML + private Label second; + @FXML + private Label date; + @FXML + private Label contactCount; + @FXML + private BarChart chart; + @FXML + private ListView availableList; + + /** + * Creates a {@code HomeCard} with the given Person List. + */ + public HomeCard(ObservableList personList) { + super(FXML); + this.personList = personList; + this.contactCount.setText(String.valueOf(personList.size())); + setUpTimeline(); + setUpMoneyChart(); + setUpAvailableTodayList(); + playAnimation(); + } + + private void setUpMoneyChart() { + XYChart.Series series = new XYChart.Series<>(); + series.getData().add(new XYChart.Data<>(MONEY_OWED_LABEL, getTotalDebt())); + series.getData().add(new XYChart.Data<>(OWED_MONEY_LABEL, getTotalCredit())); + chart.getData().add(series); + chart.setCategoryGap(CATEGORY_GAP); + chart.setBarGap(BAR_GAP); + chart.setLegendVisible(false); + } + + /** + * Sets up a 1-second interval to update the time card. + */ + private void setUpTimeline() { + time.setText(LocalDateTime.now().format(timeFormatter)); + second.setText(LocalDateTime.now().format(secondFormatter)); + date.setText(LocalDateTime.now().format(dateFormatter)); + + Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> { + LocalDateTime currentTime = LocalDateTime.now(); + time.setText(currentTime.format(timeFormatter)); + second.setText(currentTime.format(secondFormatter)); + date.setText(currentTime.format(dateFormatter)); + })); + timeline.setCycleCount(Timeline.INDEFINITE); + timeline.play(); + } + + /** + * Sets up list of people who are available today. + */ + private void setUpAvailableTodayList() { + availableList.setItems(getAvailableTodayList()); + availableList.setCellFactory(listView -> new AvailableTodayCell()); + } + + /** + * @return List of people who are available today. + */ + public ObservableList getAvailableTodayList() { + DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("EEEE"); + String today = LocalDateTime.now().format(dayFormatter); + Day filterDay = Day.getDay(today); + return personList.filtered(person -> person.getDaysAvailable().contains(filterDay)); + } + + /** + * @return Get the amount of debt the user has + */ + public double getTotalDebt() { + double result = 0.0; + for (Person person : personList) { + if (person.getMoneyOwed().userOwesMoney()) { + result += person.getMoneyOwed().getAbsoluteAmount(); + } + } + return result; + } + + /** + * @return Get the amount of credit the user has + */ + public double getTotalCredit() { + double result = 0.0; + for (Person person : personList) { + if (!person.getMoneyOwed().userOwesMoney()) { + result += person.getMoneyOwed().getAbsoluteAmount(); + } + } + return result; + } + + /** + * Play the card's animation + */ + public void playAnimation() { + fadeInTransition.playFromStart(); + moveTransition.playFromStart(); + bounceBackTransition.playFromStart(); + } + + + static class AvailableTodayCell extends ListCell { + @Override + protected void updateItem(Person person, boolean empty) { + super.updateItem(person, empty); + + if (empty || person == null) { + setGraphic(null); + setText(null); + return; + } + MiniPersonCard personCard = new MiniPersonCard(person); + setGraphic(personCard.getRoot()); + } + } +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..873e5e24f40 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -1,11 +1,20 @@ package seedu.address.ui; +import static java.util.Objects.requireNonNull; + +import java.io.IOException; import java.util.logging.Logger; +import com.google.zxing.WriterException; + +import javafx.beans.binding.Bindings; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputControl; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; @@ -13,20 +22,24 @@ import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; import seedu.address.logic.Logic; +import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.ResetDebtCommand; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Person; /** * The Main Window. Provides the basic application layout containing * a menu bar and space where other JavaFX elements can be placed. */ public class MainWindow extends UiPart { - private static final String FXML = "MainWindow.fxml"; - + private static final Double PERSON_LIST_RATIO = 0.25; + private static final Integer MINIMUM_HEIGHT = 700; + private static final Integer MINIMUM_WIDTH = 1000; private final Logger logger = LogsCenter.getLogger(getClass()); - + private Image logo = new Image(this.getClass().getResourceAsStream("/images/friendfolio_logo.png")); private Stage primaryStage; private Logic logic; @@ -34,6 +47,7 @@ public class MainWindow extends UiPart { private PersonListPanel personListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + private PaymentWindow paymentWindow; @FXML private StackPane commandBoxPlaceholder; @@ -50,12 +64,14 @@ public class MainWindow extends UiPart { @FXML private StackPane statusbarPlaceholder; + @FXML + private ImageView logoImage; + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ public MainWindow(Stage primaryStage, Logic logic) { super(FXML, primaryStage); - // Set dependencies this.primaryStage = primaryStage; this.logic = logic; @@ -64,6 +80,7 @@ public MainWindow(Stage primaryStage, Logic logic) { setWindowDefaultSize(logic.getGuiSettings()); setAccelerators(); + setEscHandler(); helpWindow = new HelpWindow(); } @@ -78,6 +95,7 @@ private void setAccelerators() { /** * Sets the accelerator of a MenuItem. + * * @param keyCombination the KeyCombination value of the accelerator */ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { @@ -110,8 +128,14 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + personListPanel = new PersonListPanel(logic.getFilteredPersonList(), logic.getSortedPersonList()); + + personListPanel.getPersonListView().prefWidthProperty().bind(Bindings.createDoubleBinding(() + -> personListPanelPlaceholder.getScene().getWidth() * PERSON_LIST_RATIO, + personListPanelPlaceholder.getScene().widthProperty())); + personListPanelPlaceholder.getChildren().setAll(personListPanel.getRoot()); + + logoImage.setImage(logo); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); @@ -123,6 +147,14 @@ void fillInnerParts() { commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); } + private void setEscHandler() { + getRoot().addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode() == KeyCode.ESCAPE) { + personListPanel.resetHomeCard(); + } + }); + } + /** * Sets the default size based on {@code guiSettings}. */ @@ -133,6 +165,8 @@ private void setWindowDefaultSize(GuiSettings guiSettings) { primaryStage.setX(guiSettings.getWindowCoordinates().getX()); primaryStage.setY(guiSettings.getWindowCoordinates().getY()); } + primaryStage.setMinHeight(MINIMUM_HEIGHT); + primaryStage.setMinWidth(MINIMUM_WIDTH); } /** @@ -161,6 +195,33 @@ private void handleExit() { logic.setGuiSettings(guiSettings); helpWindow.hide(); primaryStage.hide(); + if (paymentWindow != null) { + paymentWindow.hide(); + } + } + + @FXML + private void handlePayment(Person person) { + requireNonNull(person); + if (paymentWindow != null) { + paymentWindow.hide(); + } + try { + paymentWindow = new PaymentWindow(person, () -> { + paymentWindow.hide(); + paymentWindow = null; + try { + execute(new ResetDebtCommand(person)); + } catch (CommandException e) { + return; + } + }); + paymentWindow.show(); + } catch (IOException | WriterException e) { + logger.info("An error occurred while trying to set up PaymentWindow: " + e.getMessage()); + resultDisplay.setFeedbackToUser("An error occurred while trying to set up PaymentWindow!"); + throw new RuntimeException(e.getMessage()); + } } public PersonListPanel getPersonListPanel() { @@ -175,17 +236,7 @@ public PersonListPanel getPersonListPanel() { private CommandResult executeCommand(String commandText) throws CommandException, ParseException { try { CommandResult commandResult = logic.execute(commandText); - logger.info("Result: " + commandResult.getFeedbackToUser()); - resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); - - if (commandResult.isShowHelp()) { - handleHelp(); - } - - if (commandResult.isExit()) { - handleExit(); - } - + handleCommandResult(commandResult); return commandResult; } catch (CommandException | ParseException e) { logger.info("An error occurred while executing command: " + commandText); @@ -193,4 +244,43 @@ private CommandResult executeCommand(String commandText) throws CommandException throw e; } } + + private CommandResult execute(Command command) throws CommandException { + try { + CommandResult commandResult = logic.execute(command); + handleCommandResult(commandResult); + return commandResult; + } catch (CommandException e) { + logger.info("An error occurred while executing command: " + command); + resultDisplay.setFeedbackToUser(e.getMessage()); + throw e; + } + } + + private void handleCommandResult(CommandResult commandResult) { + logger.info("Result: " + commandResult.getFeedbackToUser()); + resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + if (commandResult.getPersonToShow() != null) { + personListPanel = new PersonListPanel( + logic.getFilteredPersonList(), logic.getSortedPersonList(), commandResult.getPersonToShow()); + + personListPanel.getPersonListView().prefWidthProperty().bind(Bindings.createDoubleBinding(() + -> personListPanelPlaceholder.getScene().getWidth() * PERSON_LIST_RATIO, + personListPanelPlaceholder.getScene().widthProperty())); + personListPanelPlaceholder.getChildren().setAll(personListPanel.getRoot()); + } + + if (commandResult.isShowHelp()) { + handleHelp(); + } + + if (commandResult.isExit()) { + handleExit(); + } + + if (commandResult.isShowPayment()) { + assert (commandResult.getPersonToPay() != null); + handlePayment(commandResult.getPersonToPay()); + } + } } diff --git a/src/main/java/seedu/address/ui/MiniPersonCard.java b/src/main/java/seedu/address/ui/MiniPersonCard.java new file mode 100644 index 00000000000..dd84843dd05 --- /dev/null +++ b/src/main/java/seedu/address/ui/MiniPersonCard.java @@ -0,0 +1,43 @@ +package seedu.address.ui; + +import java.util.Comparator; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.Region; +import seedu.address.model.person.Person; + +/** + * An UI component that displays information of a {@code Person}. + */ +public class MiniPersonCard extends UiPart { + + private static final String FXML = "MiniPersonCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Person person; + @FXML + private Label name; + @FXML + private FlowPane tags; + + /** + * Creates a {@code MiniCard} with the given {@code Person}. + */ + public MiniPersonCard(Person person) { + super(FXML); + this.person = person; + name.setText(person.getName().fullName); + person.getTags().stream() + .sorted(Comparator.comparing(tag -> tag.tagName)) + .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + } +} diff --git a/src/main/java/seedu/address/ui/PaymentWindow.java b/src/main/java/seedu/address/ui/PaymentWindow.java new file mode 100644 index 00000000000..d1a46ec0ce5 --- /dev/null +++ b/src/main/java/seedu/address/ui/PaymentWindow.java @@ -0,0 +1,91 @@ +package seedu.address.ui; + +import java.io.IOException; +import java.util.logging.Logger; + +import com.google.zxing.WriterException; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.stage.Stage; +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.paynow.PayNowCode; +import seedu.address.model.person.Person; + +/** + * Controller for the Payment window + */ +public class PaymentWindow extends UiPart { + private static final String FXML = "PaymentWindow.fxml"; + private static final Logger logger = LogsCenter.getLogger(PaymentWindow.class); + private static final String HELP_TOOLTIP_TEXT = "Open your banking application and scan this QR code to pay %s!\n" + + "Note that this QR code only works if the mobile number is registered to a bank account."; + private final Runnable onResetDebt; + + @FXML + private ImageView qrCode; + @FXML + private Button cancelButton; + + @FXML + private Button resetButton; + @FXML + private Tooltip helpTooltip; + + /** + * Creates a new QrWindow. + * + * @param person The person whom the user is attempting to pay. + */ + public PaymentWindow(Person person, Runnable onResetDebt) throws IOException, WriterException { + super(FXML, new Stage()); + this.onResetDebt = onResetDebt; + Image image = new Image(PayNowCode.generatePayNowQrCode( + person.getPhone().toString(), + Math.max(0, -person.getMoneyOwed().moneyOwed))); + qrCode.setImage(image); + if (!person.getMoneyOwed().userOwesMoney()) { + resetButton.setManaged(false); + } + helpTooltip.setText(String.format(HELP_TOOLTIP_TEXT, person.getName())); + } + + + /** + * Shows the payment window. + * + * @throws IllegalStateException
    + *
  • + * if this method is called on a thread other than the JavaFX Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
+ */ + public void show() { + logger.fine("Showing payment page."); + getRoot().show(); + getRoot().centerOnScreen(); + } + + /** + * Hides the help window. + */ + public void hide() { + getRoot().hide(); + } + + public void onResetDebt() { + onResetDebt.run(); + } +} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..f05c0cdb8b0 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -2,11 +2,14 @@ import java.util.Comparator; +import javafx.animation.FadeTransition; +import javafx.animation.TranslateTransition; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import seedu.address.commons.util.AnimationUtil; import seedu.address.model.person.Person; /** @@ -25,7 +28,9 @@ public class PersonCard extends UiPart { */ public final Person person; - + private final TranslateTransition moveTransition = AnimationUtil.getMoveTransition(getRoot()); + private final TranslateTransition bounceBackTransition = AnimationUtil.getBounceBackTransition(getRoot()); + private final FadeTransition fadeInTransition = AnimationUtil.getFadeInTransition(getRoot()); @FXML private HBox cardPane; @FXML @@ -33,27 +38,39 @@ public class PersonCard extends UiPart { @FXML private Label id; @FXML - private Label phone; - @FXML - private Label address; + private FlowPane tags; @FXML - private Label email; + private Label moneyOwed; @FXML - private FlowPane tags; + private FlowPane daysAvailable; /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. */ - public PersonCard(Person person, int displayedIndex) { + public PersonCard(Person person, int displayedIndex, boolean animate) { super(FXML); this.person = person; id.setText(displayedIndex + ". "); name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + moneyOwed.setText(person.getMoneyOwed().getMessage()); + person.getDaysAvailable().stream() + .sorted(Comparator.comparing(Enum::ordinal)) + .forEach(day -> daysAvailable.getChildren().add(new Label(day.getShortForm()))); + if (animate) { + playAnimation(); + } + } + + private void playAnimation() { + fadeInTransition.playFromStart(); + moveTransition.playFromStart(); + bounceBackTransition.playFromStart(); + } + + public HBox getCardPane() { + return cardPane; } } diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index f4c501a897b..0b83cc58c65 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -2,11 +2,16 @@ import java.util.logging.Logger; +import javafx.animation.ScaleTransition; +import javafx.beans.binding.Bindings; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; +import javafx.scene.layout.Priority; import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.util.Duration; import seedu.address.commons.core.LogsCenter; import seedu.address.model.person.Person; @@ -15,24 +20,95 @@ */ public class PersonListPanel extends UiPart { private static final String FXML = "PersonListPanel.fxml"; + private static final Integer PADDING_SIZE = 80; private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); @FXML private ListView personListView; + @FXML + private VBox displayView; + + private HomeCard homeCard; + /** * Creates a {@code PersonListPanel} with the given {@code ObservableList}. */ - public PersonListPanel(ObservableList personList) { + public PersonListPanel(ObservableList personList, ObservableList sortedList) { super(FXML); personListView.setItems(personList); personListView.setCellFactory(listView -> new PersonListViewCell()); + homeCard = new HomeCard(sortedList); + + displayView.getChildren().setAll(homeCard.getRoot()); + + personListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + if (newValue == null) { + return; + } + setDisplayCard(newValue); + }); + } + + /** + * Creates a {@code PersonListPanel} with the given {@code ObservableList} and displays the {@code DisplayCard} + * for the person in the given index. + */ + public PersonListPanel(ObservableList personList, ObservableList sortedList, int index) { + this(personList, sortedList); + personListView.getSelectionModel().select(index); + } + + private void setDisplayCard(Person person) { + DisplayCard displayCard = new DisplayCard(person); + displayView.getChildren().setAll(displayCard.getRoot()); + VBox.setVgrow(displayCard.getRoot(), Priority.ALWAYS); + } + + public ListView getPersonListView() { + return personListView; + } + + /** + * Resets the display view to display the home card. This is fired when the user + * presses the escape key. + */ + public void resetHomeCard() { + displayView.getChildren().setAll(homeCard.getRoot()); + personListView.getSelectionModel().clearSelection(); + homeCard.playAnimation(); + } + + + public int getSelectedIndex() { + return personListView.getSelectionModel().getSelectedIndex(); } /** * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. */ class PersonListViewCell extends ListCell { + + private static final double HOVERED_SCALE = 1.035; // Scale for hovered item + private static final double NORMAL_SCALE = 1.0; // Normal scale + private static final double SCALE_DURATION = 200; + private ScaleTransition scaleUpTransition; + private ScaleTransition scaleDownTransition; + private boolean hasAnimation; + + public PersonListViewCell() { + this.hasAnimation = true; + scaleUpTransition = new ScaleTransition(Duration.millis(SCALE_DURATION), this); + scaleUpTransition.setToX(HOVERED_SCALE); + scaleUpTransition.setToY(HOVERED_SCALE); + scaleDownTransition = new ScaleTransition(Duration.millis(SCALE_DURATION), this); + scaleDownTransition.setToX(NORMAL_SCALE); + scaleDownTransition.setToY(NORMAL_SCALE); + + setOnMouseEntered(e -> scaleUpTransition.playFromStart()); + setOnMouseExited(e -> scaleDownTransition.playFromStart()); + } + @Override protected void updateItem(Person person, boolean empty) { super.updateItem(person, empty); @@ -40,10 +116,22 @@ protected void updateItem(Person person, boolean empty) { if (empty || person == null) { setGraphic(null); setText(null); - } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); + return; } + PersonCard personCard = new PersonCard(person, getIndex() + 1, getAnimateFlag()); + setGraphic(personCard.getRoot()); + personCard.getCardPane().prefWidthProperty().bind(Bindings.createDoubleBinding(( + ) -> personListView.getPrefWidth() - PADDING_SIZE, personListView.prefWidthProperty())); } - } + private boolean getAnimateFlag() { + if (hasAnimation) { + + hasAnimation = false; + return true; + } + return hasAnimation; + } + + } } diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/seedu/address/ui/UiPart.java index fc820e01a9c..017725f04db 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/seedu/address/ui/UiPart.java @@ -14,7 +14,9 @@ */ public abstract class UiPart { - /** Resource folder where FXML files are stored. */ + /** + * Resource folder where FXML files are stored. + */ public static final String FXML_FILE_FOLDER = "/view/"; private final FXMLLoader fxmlLoader = new FXMLLoader(); @@ -29,6 +31,7 @@ public UiPart(URL fxmlFileUrl) { /** * Constructs a UiPart using the specified FXML file within {@link #FXML_FILE_FOLDER}. + * * @see #UiPart(URL) */ public UiPart(String fxmlFileName) { @@ -45,12 +48,23 @@ public UiPart(URL fxmlFileUrl, T root) { /** * Constructs a UiPart with the specified FXML file within {@link #FXML_FILE_FOLDER} and root object. + * * @see #UiPart(URL, T) */ public UiPart(String fxmlFileName, T root) { this(getFxmlFileUrl(fxmlFileName), root); } + /** + * Returns the FXML file URL for the specified FXML file name within {@link #FXML_FILE_FOLDER}. + */ + private static URL getFxmlFileUrl(String fxmlFileName) { + requireNonNull(fxmlFileName); + String fxmlFileNameWithFolder = FXML_FILE_FOLDER + fxmlFileName; + URL fxmlFileUrl = MainApp.class.getResource(fxmlFileNameWithFolder); + return requireNonNull(fxmlFileUrl); + } + /** * Returns the root object of the scene graph of this UiPart. */ @@ -60,8 +74,9 @@ public T getRoot() { /** * Loads the object hierarchy from a FXML document. + * * @param location Location of the FXML document. - * @param root Specifies the root of the object hierarchy. + * @param root Specifies the root of the object hierarchy. */ private void loadFxmlFile(URL location, T root) { requireNonNull(location); @@ -75,14 +90,4 @@ private void loadFxmlFile(URL location, T root) { } } - /** - * Returns the FXML file URL for the specified FXML file name within {@link #FXML_FILE_FOLDER}. - */ - private static URL getFxmlFileUrl(String fxmlFileName) { - requireNonNull(fxmlFileName); - String fxmlFileNameWithFolder = FXML_FILE_FOLDER + fxmlFileName; - URL fxmlFileUrl = MainApp.class.getResource(fxmlFileNameWithFolder); - return requireNonNull(fxmlFileUrl); - } - } diff --git a/src/main/resources/fonts/Dosis-ExtraLight.ttf b/src/main/resources/fonts/Dosis-ExtraLight.ttf new file mode 100644 index 00000000000..f4d2eec6ac1 Binary files /dev/null and b/src/main/resources/fonts/Dosis-ExtraLight.ttf differ diff --git a/src/main/resources/fonts/Quicksand-VariableFont_wght.ttf b/src/main/resources/fonts/Quicksand-VariableFont_wght.ttf new file mode 100644 index 00000000000..c8680735238 Binary files /dev/null and b/src/main/resources/fonts/Quicksand-VariableFont_wght.ttf differ diff --git a/src/main/resources/fonts/SF-Pro-Display-Black.otf b/src/main/resources/fonts/SF-Pro-Display-Black.otf new file mode 100644 index 00000000000..8463d9b1632 Binary files /dev/null and b/src/main/resources/fonts/SF-Pro-Display-Black.otf differ diff --git a/src/main/resources/fonts/SF-Pro-Display-Light.otf b/src/main/resources/fonts/SF-Pro-Display-Light.otf new file mode 100644 index 00000000000..42ef1f1f3f1 Binary files /dev/null and b/src/main/resources/fonts/SF-Pro-Display-Light.otf differ diff --git a/src/main/resources/fonts/SF-Pro-Display-Medium.otf b/src/main/resources/fonts/SF-Pro-Display-Medium.otf new file mode 100644 index 00000000000..668ba74de34 Binary files /dev/null and b/src/main/resources/fonts/SF-Pro-Display-Medium.otf differ diff --git a/src/main/resources/fonts/SF-Pro-Rounded-Light.otf b/src/main/resources/fonts/SF-Pro-Rounded-Light.otf new file mode 100644 index 00000000000..55e810b2b0b Binary files /dev/null and b/src/main/resources/fonts/SF-Pro-Rounded-Light.otf differ diff --git a/src/main/resources/fonts/SF-Pro-Rounded-Medium.otf b/src/main/resources/fonts/SF-Pro-Rounded-Medium.otf new file mode 100644 index 00000000000..57208a31c94 Binary files /dev/null and b/src/main/resources/fonts/SF-Pro-Rounded-Medium.otf differ diff --git a/src/main/resources/fonts/SF-Pro.ttf b/src/main/resources/fonts/SF-Pro.ttf new file mode 100644 index 00000000000..4f88dc135ee Binary files /dev/null and b/src/main/resources/fonts/SF-Pro.ttf differ diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png index 29810cf1fd9..fe9f3c33326 100644 Binary files a/src/main/resources/images/address_book_32.png and b/src/main/resources/images/address_book_32.png differ diff --git a/src/main/resources/images/address_icon.png b/src/main/resources/images/address_icon.png new file mode 100644 index 00000000000..c20bd699b04 Binary files /dev/null and b/src/main/resources/images/address_icon.png differ diff --git a/src/main/resources/images/birthday_icon.png b/src/main/resources/images/birthday_icon.png new file mode 100644 index 00000000000..1d873517a8f Binary files /dev/null and b/src/main/resources/images/birthday_icon.png differ diff --git a/src/main/resources/images/contact_icon.png b/src/main/resources/images/contact_icon.png new file mode 100644 index 00000000000..2841c4a57e6 Binary files /dev/null and b/src/main/resources/images/contact_icon.png differ diff --git a/src/main/resources/images/day_icon.png b/src/main/resources/images/day_icon.png new file mode 100644 index 00000000000..b385e27f5b7 Binary files /dev/null and b/src/main/resources/images/day_icon.png differ diff --git a/src/main/resources/images/email_icon.png b/src/main/resources/images/email_icon.png new file mode 100644 index 00000000000..37320a8eaa7 Binary files /dev/null and b/src/main/resources/images/email_icon.png differ diff --git a/src/main/resources/images/friendfolio_logo.png b/src/main/resources/images/friendfolio_logo.png new file mode 100644 index 00000000000..0362d5902f3 Binary files /dev/null and b/src/main/resources/images/friendfolio_logo.png differ diff --git a/src/main/resources/images/money_icon.png b/src/main/resources/images/money_icon.png new file mode 100644 index 00000000000..fcf58642d26 Binary files /dev/null and b/src/main/resources/images/money_icon.png differ diff --git a/src/main/resources/images/paynowlogo.png b/src/main/resources/images/paynowlogo.png new file mode 100644 index 00000000000..63c84035558 Binary files /dev/null and b/src/main/resources/images/paynowlogo.png differ diff --git a/src/main/resources/images/phone_icon.png b/src/main/resources/images/phone_icon.png new file mode 100644 index 00000000000..da9d8bb29a2 Binary files /dev/null and b/src/main/resources/images/phone_icon.png differ diff --git a/src/main/resources/images/tag_icon.png b/src/main/resources/images/tag_icon.png new file mode 100644 index 00000000000..bbb4cbd3cf6 Binary files /dev/null and b/src/main/resources/images/tag_icon.png differ diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..47910195505 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,32 +1,36 @@ +@font-face { + +} + .background { - -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ + -fx-background-color: #e0e0e0; + background-color: #e0e0e0; /* Used in the default.html file */ } .label { -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; + -fx-font-family: "SF Pro"; -fx-text-fill: #555555; -fx-opacity: 0.9; } .label-bright { -fx-font-size: 11pt; - -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: white; + -fx-font-family: "SF Pro"; + -fx-text-fill: black; -fx-opacity: 1; } .label-header { -fx-font-size: 32pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-font-family: "SF Pro"; + -fx-text-fill: black; -fx-opacity: 1; } .text-field { -fx-font-size: 12pt; - -fx-font-family: "Segoe UI Semibold"; + -fx-font-family: "SF Pro"; } .tab-pane { @@ -41,8 +45,8 @@ .table-view { -fx-base: #1d1d1d; - -fx-control-inner-background: #1d1d1d; - -fx-background-color: #1d1d1d; + -fx-control-inner-background: #d9d9d9; + -fx-background-color: #d7d7d7; -fx-table-cell-border-color: transparent; -fx-table-header-border-color: transparent; -fx-padding: 5; @@ -66,8 +70,8 @@ .table-view .column-header .label { -fx-font-size: 20pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-font-family: "SF Pro"; + -fx-text-fill: black; -fx-alignment: center-left; -fx-opacity: 1; } @@ -77,20 +81,20 @@ } .split-pane:horizontal .split-pane-divider { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: transparent; -fx-border-color: transparent transparent transparent #4d4d4d; } .split-pane { - -fx-border-radius: 1; + -fx-border-radius: 0; -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: transparent; } .list-view { -fx-background-insets: 0; -fx-padding: 0; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: transparent; } .list-cell { @@ -100,73 +104,127 @@ } .list-cell:filled:even { - -fx-background-color: #3c3e3f; + -fx-background-color: transparent; } .list-cell:filled:odd { - -fx-background-color: #515658; + -fx-background-color: transparent; } .list-cell:filled:selected { - -fx-background-color: #424d5f; + } .list-cell:filled:selected #cardPane { - -fx-border-color: #3e7b91; + -fx-border-color: #2daddc; -fx-border-width: 1; + -fx-background-color: #edfaff; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 5, 0, 0, 3); +} + +.list-cell:filled:hover #cardPane { + -fx-border-color: rgb(168, 168, 168); } .list-cell .label { - -fx-text-fill: white; + -fx-text-fill: black; } -.cell_big_label { - -fx-font-family: "Segoe UI Semibold"; +.amount-header { + -fx-font-family: "SF Pro"; + -fx-font-size: 40px; + -fx-text-fill: #010504; +} + +.time-big-label { + -fx-font-family: "SF Pro"; + -fx-font-size: 60px; + -fx-text-fill:white; +} + +.time-small-label { + -fx-font-family: "SF Pro"; + -fx-font-size: 40px; +} + +.display_big_label { + -fx-font-family: "SF Pro"; + -fx-font-size: 45px; + -fx-text-fill: #010504; +} + +.display_small_bold_label { + + -fx-font-family: "SF Pro"; -fx-font-size: 16px; -fx-text-fill: #010504; + -fx-font-weight:bold; +} + +.display_small_label { + -fx-font-family: "SF Pro"; + -fx-font-size: 16px; + -fx-text-fill: #010504; +} + +.cell_big_label { + -fx-font-family: "SF Pro"; + -fx-font-size: 18px; + -fx-text-fill: #010504; } .cell_small_label { - -fx-font-family: "Segoe UI"; + -fx-font-family: "SF Pro"; -fx-font-size: 13px; -fx-text-fill: #010504; } +/*this is for the command text background*/ .stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: rgba(118, 118, 118, 0.5); + -fx-border-radius: 15; + -fx-background-radius: 15; } .pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); - -fx-border-top-width: 1px; + + -fx-background-radius: 20; + -fx-border-radius:20; } .status-bar { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: linear-gradient(to bottom right, #7BD7E5, #1B96C0 50%, #7BE5D2); } .result-display { - -fx-background-color: transparent; - -fx-font-family: "Segoe UI Light"; + -fx-font-family: "SF Pro"; -fx-font-size: 13pt; -fx-text-fill: white; + -fx-background-color: transparent; + -fx-background-insets: 0; +} + +.result-display .content{ + -fx-background-insets: 0; + -fx-background-color: transparent; + -fx-background-insets: 0; } .result-display .label { -fx-text-fill: black !important; + -fx-background-color: transparent; + -fx-background-insets: 0; } .status-bar .label { - -fx-font-family: "Segoe UI Light"; + -fx-font-family: "SF Pro"; -fx-text-fill: white; -fx-padding: 4px; -fx-pref-height: 30px; } .status-bar-with-border { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 25%); + -fx-background-color: linear-gradient(to bottom right, #7BD7E5, #1B96C0 50%, #7BE5D2); -fx-border-width: 1px; } @@ -175,17 +233,16 @@ } .grid-pane { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 30%); + -fx-background-color: rgb(128, 128, 128); -fx-border-width: 1px; } .grid-pane .stack-pane { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: rgb(128, 128, 128); } .context-menu { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: rgb(128, 128, 128); } .context-menu .label { @@ -193,12 +250,12 @@ } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: rgb(128, 128, 128); } .menu-bar .label { -fx-font-size: 14pt; - -fx-font-family: "Segoe UI Light"; + -fx-font-family: "SF Pro"; -fx-text-fill: white; -fx-opacity: 0.9; } @@ -282,12 +339,17 @@ } .scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: transparent; +} + +.scroll-bar:vertical { + -fx-pref-width: 15px; } .scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: #cccccc; -fx-background-insets: 3; + -fx-background-radius: 10; } .scroll-bar .increment-button, .scroll-bar .decrement-button { @@ -307,20 +369,59 @@ -fx-padding: 8 1 8 1; } -#cardPane { +#miniPane { -fx-background-color: transparent; - -fx-border-width: 0; +} + +#cardPane { + -fx-background-color: white; + -fx-background-radius: 10; + -fx-border-width: 0.3; + -fx-border-color:rgb(201, 201, 201); + -fx-border-radius: 10; +} + +#topPane{ + -fx-border-color: rgb(184, 184, 184); + -fx-border-width: 0.8; +} + +.white-pane { + -fx-background-color: white; + -fx-background-radius: 20; + -fx-border-width: 0.5; + -fx-border-color:rgb(201, 201, 201); + -fx-border-radius: 20; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 5, 0, 0, 3); +} + +#notePane { + -fx-background-color: rgb(255, 249, 228); + -fx-background-radius: 10; + -fx-border-width: 0.5; + -fx-border-color:rgb(201, 201, 201); + -fx-border-radius: 10; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 5, 0, 0, 3); +} + +#contactAmount { + -fx-font-size: 40px; +} + +#contactTop, +#contactBottom, +#contactAmount { + } #commandTypeLabel { -fx-font-size: 11px; - -fx-text-fill: #F70D1A; + /*-fx-text-fill: #F70D1A;*/ } #commandTextField { -fx-background-color: transparent #383838 transparent #383838; -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; -fx-border-insets: 0; -fx-border-width: 1; -fx-font-family: "Segoe UI Light"; @@ -332,11 +433,27 @@ -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); } +/*this is for the result display block*/ #resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; - -fx-background-radius: 0; + -fx-background-color: rgba(116, 51, 163, 0.371); + -fx-background-radius:10; + -fx-background-insets: 0; +} + +#resultDisplay .viewport { + -fx-background-color: transparent; + -fx-background-insets: 0; +} + +#resultDisplay { + + -fx-background-color: rgba(50, 50, 50, 0.5); + -fx-background-insets: 0; + -fx-background-radius: 20; + -fx-border-radius:20; } +#daysAvailable, #tags { -fx-hgap: 7; -fx-vgap: 3; @@ -344,9 +461,56 @@ #tags .label { -fx-text-fill: white; - -fx-background-color: #3e7b91; + -fx-background-color: #2658c4; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +#daysAvailable { + -fx-hgap: 7; + -fx-vgap: 3; +} + +#daysAvailable .label { + -fx-text-fill: white; + -fx-background-color: #7334a7; -fx-padding: 1 3 1 3; -fx-border-radius: 2; -fx-background-radius: 2; -fx-font-size: 11; } + +#mainPanel { + -fx-background-color: #EFEFF4; +} + +#timePane { + -fx-background-color: linear-gradient(to bottom right, #269bad, #167798 50%, #57af9f); + -fx-background-radius: 20; + -fx-border-width: 0.5; + -fx-border-color:rgb(201, 201, 201); + -fx-border-radius: 20; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 5, 0, 0, 3); +} + +#time, +#second, +#date { + -fx-text-fill:white; +} + +#date{ + -fx-font-size: 20; +} + +.chart-bar { + -fx-bar-fill: rgb(110, 186, 221); + -fx-background-color: linear-gradient(to bottom right, #269bad, #167798 50%, #57af9f); +} + +.axis { + -fx-tick-label-font-size: 15; + -fx-tick-label-font-family: "SF Pro"; +} diff --git a/src/main/resources/view/DisplayCard.fxml b/src/main/resources/view/DisplayCard.fxml new file mode 100644 index 00000000000..8c9d4d4ff63 --- /dev/null +++ b/src/main/resources/view/DisplayCard.fxml @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index bfe82a85964..2bc2e990d9c 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -1,18 +1,18 @@ .error { - -fx-text-fill: #d06651 !important; /* The error class should always override the default text-fill style */ + -fx-text-fill: #ff534c !important; /* The error class should always override the default text-fill style */ } .list-cell:empty { /* Empty cells will not have alternating colours */ - -fx-background: #383838; + -fx-background: transparent; } .tag-selector { -fx-border-width: 1; - -fx-border-color: white; - -fx-border-radius: 3; - -fx-background-radius: 3; + -fx-border-color: transparent; + -fx-border-radius: 20; + -fx-background-radius: 20; } .tooltip-text { diff --git a/src/main/resources/view/HomeCard.fxml b/src/main/resources/view/HomeCard.fxml new file mode 100644 index 00000000000..4c35689c963 --- /dev/null +++ b/src/main/resources/view/HomeCard.fxml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..316fd72912b 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -2,59 +2,87 @@ - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - + + + + + + + + + + + + - - - - - - + + + - - - - + + + + + + diff --git a/src/main/resources/view/MiniPersonCard.fxml b/src/main/resources/view/MiniPersonCard.fxml new file mode 100644 index 00000000000..34af125b548 --- /dev/null +++ b/src/main/resources/view/MiniPersonCard.fxml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PaymentWindow.css b/src/main/resources/view/PaymentWindow.css new file mode 100644 index 00000000000..aee0e3498dd --- /dev/null +++ b/src/main/resources/view/PaymentWindow.css @@ -0,0 +1,9 @@ +.payment-page-btn { + -fx-background-color: #1B96C0; + -fx-padding: 10; + -fx-border-radius: 10; + -fx-font-size: 16; + -fx-text-fill: white; + -fx-pref-width: 150; + -fx-cursor: hand; +} diff --git a/src/main/resources/view/PaymentWindow.fxml b/src/main/resources/view/PaymentWindow.fxml new file mode 100644 index 00000000000..f8df3c9ef8b --- /dev/null +++ b/src/main/resources/view/PaymentWindow.fxml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f5e812e25e6..9ffc6c8d91d 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -7,30 +7,50 @@ + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml index a1bb6bbace8..94bf67f538a 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/PersonListPanel.fxml @@ -1,8 +1,30 @@ + + + + - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 01b691792a9..d182b868bfb 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -2,8 +2,10 @@ - - -