diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..2f4ae79d69a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,11 +10,13 @@ src/main/resources/docs/ # Storage/log files /data/ +/bin/ /config.json /preferences.json /*.log.* hs_err_pid[0-9]*.log + # Test sandbox files src/test/data/sandbox/ diff --git a/README.md b/README.md index 13f5c77403f..e4b0a0bfd52 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![CI Status](https://github.com/AY2324S2-CS2103T-W13-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S2-CS2103T-W13-1/tp/actions) + +[![codecov](https://codecov.io/gh/AY2324S2-CS2103T-W13-1/tp/graph/badge.svg?token=MZAGQZIFNS)](https://codecov.io/gh/AY2324S2-CS2103T-W13-1/tp) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
+* This is **LoanGuard Pro, an application that helps business owners manage clients and their loan details**.
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. + * to keep track of the items you have loaned out + * to view your history of loans by client +* The project builds on an existing Address Book used for managing contact details, **adding in a loan tracker functionality**. + * It is **written in OOP fashion**. * 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. +* It is named `LoanGuard Pro` because it represents a more powerful version of an address book, that can also manage the loans of your contacts. +* For the detailed documentation of this project, see the **[LoanGuard Pro Product Website](https://ay2324s2-cs2103t-w13-1.github.io/tp/)**. +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). diff --git a/build.gradle b/build.gradle index a2951cc709e..8d5f0dd6d33 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ plugins { id 'com.github.johnrengelman.shadow' version '7.1.2' id 'application' id 'jacoco' + id 'org.jetbrains.kotlin.jvm' version '1.9.21' } mainClassName = 'seedu.address.Main' @@ -63,10 +64,25 @@ dependencies { testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } shadowJar { archiveFileName = 'addressbook.jar' } +run { + enableAssertions = true +} + defaultTasks 'clean', 'test' +compileKotlin { + kotlinOptions { + jvmTarget = "11" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "11" + } +} diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..41ed704fe65 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,57 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Khor Jun Wei - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/kjw142857)] +[[portfolio](https://github.com/kjw142857)] -* Role: Project Advisor +* Role: Developer for basic loan implementation + debugging and testing. +* Responsibilities: Implementing the `linkloan` and `editloan` commands; +finding bugs and pushing fixes; writing test code. -### Jane Doe +### Kyal Sin Min Thet - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/marcus-ny)] +[[portfolio](http://github.com/marcus-ny)] -* Role: Team Lead -* Responsibilities: UI +* Role: Team Lead + Frontend. +* Responsibilities: setting up repo + CI workflow; +Implementing view options for different panels - loans, contacts, analytics dashboards. -### Johnny Doe +### Teoh Tze Tzun - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](https://github.com/Joseph31416)] [[portfolio](https://github.com/Joseph31416)] -* Role: Developer -* Responsibilities: Data +* Role: Developer for data classes and UG documentation. +* Responsibilities: +Implementing the `Loan`, `LoanRecords` (now refactored as `UniqueLoanList`) and `Analytics` classes from scratch; +writing the user guide. -### Jean Doe +### Wang Junwu - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/narwhalsilent)] +[[portfolio](http://github.com/narwhalsilent)] -* Role: Developer -* Responsibilities: Dev Ops + Threading +* Role: Developer for loan architecture and UI integration + DG documentation. +* Responsibilities: Implementing loan architecture as `UniqueLoanList` in the `Model`; +implementing the `viewloan` command; +writing the developer guide. -### James Doe +### Zhang Xiaorui - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/xiaorui-ui)] +[[portfolio](https://github.com/xiaorui-ui)] -* Role: Developer -* Responsibilities: UI +* Role: Developer for basic loan management and DG documentation. +* Responsibilities: Implementing `deleteloan`, `markloan`, and `unmarkloan` commands; writing the developer guide. diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 1b56bb5d31b..4c74b50ec53 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,14 +2,59 @@ layout: page title: Developer Guide --- -* Table of Contents -{:toc} + +[//]: <> (comment, To-do, make working links) +[//]: <> (more To-dos: Instructions for Manual Testing, Appendix: Effort, Planned Enhancements) + +## Table of Contents +[1. Acknowledgements](#acknowledgements)
+[2. Setting up, getting started](#setting-up-getting-started)
+[3. Design](#design)
+- [3.1 Architecture](#architecture)
+- [3.2 UI component](#ui-component)
+- [3.3 Logic component](#logic-component)
+- [3.4 Model component](#model-component)
+- [3.5 Storage component](#storage-component)
+- [3.6 Common classes](#common-classes)
+ +[4. Enhancements Added](#enhancements-added)
+- [4.1 Loan Analytics - Joseph](#loan-analytics---joseph)
+- [4.2 Delete Loan - Xiaorui](#delete-loan---xiaorui)
+- [4.3 Loan view command - Wang Junwu](#loan-view-command---wang-junwu)
+- [4.4 Loan view GUI - Kyal Sin Min Thet](#loan-view-gui---kyal-sin-min-thet)
+- [4.5 Linking a loan - Khor Jun Wei](#linking-a-loan---khor-jun-wei)
+ +[5. Documentation, logging, testing, configuration, dev-ops](#documentation-logging-testing-configuration-dev-ops)
+[6. Appendix: Requirements](#appendix-requirements)
+ +- [6.1 Product scope](#product-scope)
+- [6.2 User stories](#user-stories)
+- [6.3 Use cases](#use-cases)
+- [6.4 Non-Functional Requirements](#non-functional-requirements)
+ +[7. Appendix: Instructions for manual testing](#appendix-instructions-for-manual-testing)
+- [7.1 Launch and shutdown](#launch-and-shutdown)
+- [7.2 Deleting a person](#deleting-a-person)
+- [7.3 Linking a loan](#linking-a-loan)
+- [7.4 Viewing loans](#viewing-loans)
+- [7.5 Marking and unmarking a loan](#marking-and-unmarking-a-loan)
+- [7.6 Editing a loan](#editing-a-loan)
+- [7.7 Deleting a loan](#deleting-a-loan)
+- [7.8 Analytics Command](#analytics-command)
+- [7.9 Saving data](#saving-data)
+- [7.10 Exiting the app](#exiting-the-app)
+ +[8. Appendix: Effort](#appendix-effort)
+[9. Appendix: Planned Enhancements](#appendix-planned-enhancements)
+[10. Glossary](#glossary)
+ -------------------------------------------------------------------------------------------------------------------- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +This project is modified upon the [AddressBook-Level3 project](https://github.com/se-edu/addressbook-level3) project created by the SE-EDU initiative, +as well as the [tutorials](https://nus-cs2103-ay2021s1.github.io/tp/tutorials/AddRemark.html) and guides provided. -------------------------------------------------------------------------------------------------------------------- @@ -23,7 +68,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 +83,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-W13-1/tp/tree/master/src/main/java/seedu/address/Main.java) +and [`MainApp`](https://github.com/AY2324S2-CS2103T-W13-1/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 +102,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,30 +124,45 @@ The sections below give more details of each component. ### UI component -The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) +The **API** of this component is specified +in [`Ui.java`](https://github.com/AY2324S2-CS2103T-W13-1/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` 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 consists of a `MainWindow` that has three different views. Each view consists of parts which +inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the +visible GUI. +1. The **person tab view** is made up of four parts: +`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter`. +2. The **loan tab view** is made up of four parts: +`CommandBox`, `ResultDisplay`, `LoanListPanel`, `StatusBarFooter`. +3. The **analytics tab view** is made up of four parts: +`CommandBox`, `ResultDisplay`, `AnalyticsPanel`, `StatusBarFooter`. + +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-W13-1/tp/tree/master/src/main/java/seedu/address/ui/MainWindow.java) +is specified +in [`MainWindow.fxml`](https://github.com/AY2324S2-CS2103T-W13-1/tp/tree/master/src/main/resources/view/MainWindow.fxml) The `UI` component, -* executes user commands using the `Logic` component. +* executes user commands using the `Logic` component, which could switch between the different views. * listens for changes to `Model` data so that the UI can be updated with the modified data. * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +* depends on some classes in the `Model` component, +as it displays `Person`, `Loan` and `Analytics` object residing in the `Model`. ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2324S2-CS2103T-W13-1/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 +171,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 +185,17 @@ 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. ### 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-W13-1/tp/tree/master/src/main/java/seedu/address/model/Model.java) @@ -123,9 +203,18 @@ 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 the loan records data i.e., all `Loan` objects (which are contained in a `UniqueLoanList` 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 the currently 'selected' `Loan` objects (e.g., results of a view loan command) as a separate + _filtered_ and _sorted_ 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,116 +222,255 @@ 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-W13-1/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 Classes used by multiple components are in the `seedu.addressbook.commons` package. --------------------------------------------------------------------------------------------------------------------- +## Enhancements Added -## **Implementation** +[//]: <> (change it to the analytics function) -This section describes some noteworthy details on how certain features are implemented. +### Loan Analytics - Joseph -### \[Proposed\] Undo/redo feature +#### Implementation -#### Proposed Implementation +The `Analytics` class handles the analysis of a `ObservableList` object. +This class can only be instantiated by calling the static method `getAnalytics(ObservableList loanList)`. -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: +It contains the following fields that can prove to be useful for the user: -* `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. +* `numLoans`: total number of loans +* `numOverdueLoans`: total number of overdue loans +* `numActiveLoans`: total number of active loans +* `propOverdueLoans`: proportion of loans that are overdue over active loans +* `propActiveLoans`: proportion of loans that are active over total loans +* `totalValueLoaned`: total value of all loans +* `totalValueOverdue`: total value of all overdue loans +* `totalValueActive`: total value of all active loans +* `averageLoanValue`: average loan value of all loans +* `averageOverdueValue`: average loan value of all overdue loans +* `averageActiveValue`: average loan value of all active loans +* `earliestLoanDate`: earliest loan date of all loans +* `earliestReturnDate`: earliest return date of active loans +* `latestLoanDate`: latest loan date of all loans +* `latestReturnDate`: latest return date of active loans -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +The `AnalyticsCommand` class handles the viewing of analytics of any one person within the current contact list in view. +The following shows how the analytics class is used in the execution of a command to view the analytics of a person: +![AnalyticsSequenceDiagram](images/AnalyticsSequenceDiagram.png) -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +#### Design considerations: -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. +##### Aspect: Initialization of the Analytics object: -![UndoRedoState0](images/UndoRedoState0.png) +* **Alternative 1 (current choice):** Initialize the Analytics class using a factory method. + * Pros: Hide the constructor from the user, ensure that fields are initialized correctly, more defensive. + * Cons: Slightly more complex than a public constructor. -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. +* **Alternative 2:** Initialize the Analytics class using a public constructor. + * Pros: More straightforward to use. + * Cons: User may not initialize the fields correctly, less defensive. -![UndoRedoState1](images/UndoRedoState1.png) +##### Aspect: What fields to include in the analytics: -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`. +* **Alternative 1 (current choice):** Include all fields. + * Pros: Satisfy 'ask, don't tell' principle. + * Cons: Possibly result in redundant information for the GUI developer. -![UndoRedoState2](images/UndoRedoState2.png) +* **Alternative 2:** Include only raw data (e.g. total number of loans, total value of all loans). + * Pros: No redundant information. + * Cons: GUI developer has to calculate the analytics themselves, violating 'ask, don't tell' principle. -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +### Delete Loan - Xiaorui -
+#### Implementation -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. +The `DeleteLoanCommand` class handles the deletion of a loan from the current contact in view, and executes the command +after the input is parsed and transformed into an appropriate format. +The parsing of the command is done by the `DeleteLoanCommandParser` class, which is responsible for parsing the user +input. -![UndoRedoState3](images/UndoRedoState3.png) +The `DeleteLoanCommand` class is instantiated in the `DeleteLoanCommandParser` class, while the +`DeleteLoanCommandParser` is instantiated in the `AddressBookParser` class. Both classes are instantiated when the user +enters a `deleteloan` command, which needs to be of the format `deleteloan INDEX` +where INDEX is the index of the loan to be deleted (which is a positive whole number). -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather -than attempting to perform the undo. +The `DeleteLoanCommand` class contains the following fields which can prove to be useful for the user: -
+* `loanIndex`: the index of the loan to be deleted +* Several string fields that are displayed to the user under different scenarios. -The following sequence diagram shows how an undo operation goes through the `Logic` component: +The `DeleteLoanCommandParser` class does not contain any fields. -![UndoSequenceDiagram](images/UndoSequenceDiagram-Logic.png) +Sequence diagram for the deletion of a loan: -
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +![DeleteLoanSequenceDiagram](images/DeleteLoanSequenceDiagram.png) -
+#### Design considerations: -Similarly, how an undo operation goes through the `Model` component is shown below: +##### Aspect: How the command is executed: -![UndoSequenceDiagram](images/UndoSequenceDiagram-Model.png) +* **Alternative 1 (current choice):** The `DeleteLoanCommand` class is responsible for executing the command only. + * Pros: Follows the Single Responsibility Principle. Simpler to debug. + * Cons: May result in more classes. +* **Alternative 2:** The `LogicManager` class is responsible for executing the command. + * Pros: More centralized command execution. + * Cons: May result in the `LogicManager` class becoming too large. This also goes against various SWE principles, + and makes the code harder to maintain. -The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +##### Aspect: How the command is parsed: -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +* **Alternative 1 (current choice):** The `DeleteLoanCommandParser` class is responsible for parsing the command. + * Pros: Follows the Single Responsibility Principle. Simpler to debug. + * Cons: May result in more classes. +* **Alternative 2:** The `AddressBookParser` class is responsible for parsing the command. + * Pros: More centralized command parsing. + * Cons: May result in the `AddressBookParser` class becoming too large. This also goes against various SWE + principles, and makes the code harder to maintain. -
+### Loan view command - Wang Junwu -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. +#### Implementation -![UndoRedoState4](images/UndoRedoState4.png) +The `ViewLoanCommand` class handles the viewing of all loans attached to a contact, and executes the command after the +input is parsed and transformed into an appropriate format. +The parsing of the command is done by the `ViewLoanCommandParser` class, +which is responsible for parsing the user input. -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. +The `ViewLoanCommand` class is instantiated in the `ViewLoanCommandParser` class, while the +`ViewLoanCommandParser` is instantiated in the `AddressBookParser` class. Both classes are instantiated when the user +enters a `viewloan` command, which needs to be of the format `viewloan INDEX` +where INDEX is the index of the person whose loans are to be viewed, a positive whole number. -![UndoRedoState5](images/UndoRedoState5.png) +The `ViewLoanCommand` class contains the following fields which can prove to be useful for the user: +* `targetIndex`: the index of the person whose loans are to be viewed +* Some string fields that are displayed to the user under different scenarios. -The following activity diagram summarizes what happens when a user executes a new command: +The `ViewLoanCommandParser` class does not contain any fields. - +Sequence diagram for the viewing of loans: +![ViewLoanSequenceDiagram](images/ViewLoanSequenceDiagram.png) #### Design considerations: -**Aspect: How undo & redo executes:** +##### Aspect: How the loan list is accessed: + +* **Alternative 1 (current choice):** The `ViewLoanCommand.execute()` method sequentially obtains the person list, +the person, and then the loan list. + * Pros: Minimises the layers of methods to follow through. + * Cons: May violate the SLAP principle and the Law of Demeter. +* **Alternative 2:** The `ViewLoanCommand.execute()` method obtains the loan list directly from the `Model`. + * Pros: Follows the SLAP principle and the Law of Demeter. Better use of abstraction. + * Cons: May result in longer method chains. + +##### Aspect: How the command is parsed: + +* **Alternative 1 (current choice):** The `ViewLoanCommandParser` class is responsible for parsing the command. + * Pros: Follows the Single Responsibility Principle. Simpler to debug. + * Cons: May result in more classes. +* **Alternative 2:** The `AddressBookParser` class is responsible for parsing the command. + * Pros: More centralized command parsing. + * Cons: May result in the `AddressBookParser` class becoming too large. This also goes against various SWE + principles + ,and makes the code harder to maintain. + +### Loan view GUI - Kyal Sin Min Thet + +#### Implementation + +The GUI component to display loans attached to a contact is implemented in tandem with the `viewloan` command. + +The update behaviour is achieved through the use of `ObservableList` objects in the `Model` component. The `MainWindow` +component listens for changes in the `ObservableList` objects and updates the GUI accordingly. + +The `LoanListPanel` (similar to `PersonListPanel`) is responsible for displaying the list of loans attached to a +contact. It generates new `LoanCard` objects according to the `loanList` in the `Model` class. +To accommodate the new GUI component, the `MainWindow.java` file is updated to include the new `LoanListPanel`. + +To ensure that only either the loan list or the person list is displayed, an additional `BooleanProperty` is added to +the `Model` +component to act as a flag to indicate which list is currently being displayed. This flag is updated by corresponding +commands. +For instance, commands such as `list` will toggle the flag to false, while `viewloan` will toggle the flag to true. +This update switches the display between the two lists inside `MainWindow`. + +#### Design Considerations + +##### Aspect: How the GUI is updated -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +* **Alternative 1 (current choice):** The GUI updates are done by the `Model` component's observable properties. + * Pros: Follows the observer design pattern, reducing coupling between the `Model` and `MainWindow` components. + * Cons: The GUI updates are restricted to the observable properties of the `Model` component. -* **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. +* **Alternative 2:** The GUI updates are done by the `MainWindow` component. + * Pros: More explicit control over the GUI updates. + * Cons: `Model` needs a reference to `MainWindow` to update the GUI directly. This increases coupling between the + components. -_{more aspects and alternatives to be added}_ +### Linking a loan - Khor Jun Wei -### \[Proposed\] Data archiving +#### Implementation -_{Explain here how the data archiving feature will be implemented}_ +Linking a loan is implemented through the `linkloan` command, which adds a loan to the list. This was achieved through +the addition of a `LinkLoanCommand` class, which handles the retrieval of targets (the target loan and person) +and the execution of the command. +The `LinkLoanCommand` class is instantiated in the `LinkLoanCommandParser` class, while the +`LinkLoanCommandParser` is instantiated in the `AddressBookParser` class. Both classes are instantiated when the user +enters a `linkloan` command, which needs to be of the format `linkloan INDEX v/VALUE s/START_DATE r/RETURN_DATE` +where INDEX (a positive whole number) is the index of the person, VALUE (a positive decimal number) is the value of the loan, +and START_DATE and RETURN_DATE (both dates in the format yyyy-mm-dd) are the start and return dates of the loan respectively. + +The `LinkLoanCommand` class contains the following fields which can prove to be useful for the user: + +* `toLink`: the description of the loan to be linked as a `LinkLoanDescriptor`, including its value, start date and return date +* `linkTarget`: the index of the person to link the loan to +* Several string fields that are displayed to the user under different scenarios. + +Sequence diagram for the linking of loans: +![LinkLoanSequenceDiagram](images/LinkLoanSequenceDiagram.png) + + +#### Design Considerations + +##### Aspect: How the command is executed: + +* **Alternative 1 (current choice):** The `LinkLoanCommand` class is responsible for executing the command only. + * Pros: Follows the Single Responsibility Principle. Simpler to debug. + * Cons: May result in more classes. +* **Alternative 2:** The `LogicManager` class is responsible for executing the command. + * Pros: More centralized command execution. + * Cons: May result in the `LogicManager` class becoming too large. This also goes against various SWE principles, + and makes the code harder to maintain. + +##### Aspect: How the command parameters (i.e. loan details) are stored: + +* **Alternative 1 (current choice):** The parameters are stored in a temporary `LinkLoanDescriptor`, +which is then passed into the `LinkLoanCommand`. + * Pros: As it follows the style of the `EditCommand`, it is simpler to debug. In addition, it follows the Single Responsibility Principle + as then the `LinkLoanDescriptor` will be in charge of handling the loan details. + * Cons: An additional `LinkLoanDescriptor` nested class is needed in `LinkLoanCommand`. +* **Alternative 2:** Each detail about the loan (e.g. value, start date) is stored +separately in the `LinkLoanCommand`. + * Pros: Eliminates the need for any additional classes. + * Cons: Reduces the ease for potentially adding new loan details in the future, + as then each time a new field would need to be added to `LinkLoanCommand`. In addition, + may be more difficult to debug due to novelty of code. -------------------------------------------------------------------------------------------------------------------- @@ -262,71 +490,184 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +The target user is a business person who satisfies the following criteria: + +* has a need to manage a significant number of contacts; +* prefers desktop apps over other types of apps; +* can type fast; +* prefers typing to mouse interactions; +* is reasonably comfortable using CLI apps; +* wants to manage contacts faster than a typical mouse/GUI driven app. -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +Typically, they want to answer the following questions quickly: +* How much cash was loaned? +* To whom it was loaned to? +* When the person is due to return the loan? +* When did the person last loan? + +**Value proposition**: Manage contacts faster than a typical mouse/GUI driven app + +Our software streamlines loanee management, preventing profit loss and enhancing loanee engagement. +It simplifies loan categorization and tracks product quality post-return, ensuring efficient +decision-making. Some boundaries include no detailed client reviews or personal loan management, +as we focus solely on business loans and contact management for a select client group. ### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | - -*{More to be added}* +| Priority | As a …​ | I want to …​ | So that I can…​ | +|----------|---------------------------------------------------|---------------------------------------------------------|-------------------------------------------------------------------------| +| `* * *` | User who loans cash out regularly | Add loan details (loanee /cash value) to the contact | remember to collect debts at a later time | +| `* * *` | User who loans cash out regularly | Add a deadline to a loan | chase after people more easily | +| `* *` | User who loans cash out regularly | View my past loans | know how much cash to expect in the near future | +| `* * *` | User who loans cash out regularly | View my past loans | decide whether to loan to a client again | +| `* *` | User who loans cash out regularly | See the overdue loans easily | chase after people more easily | +| `* * *` | Busy user | Keep track of all my loanees(view) | save time and use it for more meaningful activities | +| `* * *` | Busy user | Quickly view a summary of all outstanding loans(view) | have an overview without going through each contact individually | +| `* * *` | User with a dynamic network | Delete loan | my records always reflect the current status of each loan | +| `* *` | User with a dynamic network | Update loan entries as situations change | my records always reflect the current status of each loan | +| `* *` | First time user | See the available commands/usage manual | familiarize with the command structure | +| `*` | Intermediate user | Learn shortcuts to commands | save time in the future | +| `* *` | Experienced user | Omit certain parts of the CLI commands | perform tasks more efficiently and quickly | +| `* *` | Forgetful user | Get reminders to collect cash | collect cash promptly | +| `* *` | Organised user | Have a system to manage my loanees | | +| `* *` | Detail-oriented user | Add notes to each loan entry | I can record specific details or conditions of the loan | +| `* ` | User who lends frequently to the same individuals | View aggregated loan statistics per contact | I can understand our loan history at a glance | +| `* *` | Frequent lender | Track the history of cash loaned to and from a contact | I can reference past transactions during conversations | +| `* *` | User looking to minimize losses | Flag high-risk loans based on past behavior | I can make more informed lending decisions in the future | +| `* *` | User concerned with privacy | Mark certain contacts or loan entries as private | they are not visible during casual browsing of the address book | +| `* *` | Proactive user | Mark certain contacts or loan entries as private | they are not visible during casual browsing of the address book | +| `*` | User who appreciates convenience | Integrate the application with my calendar | loan due dates and follow-up reminders are automatically added | +| `*` | User who values clarity | Print or export detailed loan reports | I can have a physical or digital record for personal use or discussions | +| `*` | Collaborative user | Share loan entries with another user of the application | we can co-manage loans or items owned jointly | +| `*` | User with international contacts | Store and view currency information for cash loans | I can accurately track and manage international loans | +| `*` | User who appreciates personalization | Customize the notification settings for loan reminders | I can receive them through my preferred communication channel | ### 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 `LoanGuard Pro` and the **Actor** is the `user`, unless specified +otherwise) -**Use case: Delete a person** +#### Use case: UC1 - Delete a contact -**MSS** +Precondition: `list` command shows a numbered list of contacts. -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 +#### MSS - Use case ends. +1. User requests to delete a contact, specifying the index. +2. System deletes the contact from the address book. +3. System shows the contact that was deleted in the status message. -**Extensions** +Use case ends. -* 2a. The list is empty. +#### Extensions +- 1a. Index is invalid (e.g. negative, zero, or larger than the list size). + - 1a1. System shows an error message in the status message. + - Use case ends. - Use case ends. +#### Use case: UC2 - Find a person by name -* 3a. The given index is invalid. +#### MSS - * 3a1. AddressBook shows an error message. +1. User searches for a contact with desired prompt. +2. System shows the list of contacts that match the prompt. - Use case resumes at step 2. +Use case ends. -*{More to be added}* +#### Extensions -### Non-Functional Requirements +- 1a. User searches for a contact using an empty prompt. + - 1a1. System shows an error message in the status message. + - Use case ends. + +- 1b. No contact matches the prompt. + - 1b1. System shows a message in the status message that no contact matches the prompt. + - Use case ends. + +#### Use case: UC3 - Link a loan to contact + +#### MSS + +1. User links a contact with a loan, specifying the contact index and loan details. +2. System links the loan to the contact. +3. System shows the contact and the loan that was linked successfully in the status message. + +Use case ends. + +#### Extensions + +- 1a. Person index is invalid (e.g. negative, zero, or larger than the list size). + - 1a1. System shows an error message in the status message. + - Use case ends. + +- 1b. Loan details are invalid (e.g. empty, incomplete, wrong format). + - 1b1. System shows an error message that the loan details are invalid. + - Use case ends. + +#### Use case: UC4 - View all loans linked to particular contact + +#### MSS + +1. User requests to view all loans linked to a particular contact. +2. System shows the list of loans linked to the contact. + Use case ends. + +#### Extensions -1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +- 1a. Index is invalid (e.g. negative, zero, or larger than the list size). + - 1a1. System shows an error message in the status message. + - Use case ends. -*{More to be added}* +#### Use case: UC5 - Delete a loan from contact -### Glossary +#### MSS -* **Mainstream OS**: Windows, Linux, Unix, MacOS -* **Private contact detail**: A contact detail that is not meant to be shared with others +1. User views all loans linked to the contact (UC4). +2. User issues `deleteloan` command with the index of loan to be cleared. +3. System deletes the loan from the contact. +4. System shows the contact and the loan that was deleted successfully in the status message. + Use case ends. + +#### Extensions + +- 1a. Index is invalid (e.g. negative, zero, or larger than the list size). + - 1a1. System shows an error message in the status message. + - Use case ends. + +#### Use case: UC6 - Mark a loan as returned + +#### MSS + +1. User views all loans linked to the contact (UC4). +2. User marks a loan as returned specifying the loan index. +3. System marks the loan as returned. +4. System shows the contact and the loan that was marked as returned successfully in the status message. + Use case ends. + +#### Extensions + +- 1a. Index is invalid (e.g. negative, zero, or larger than the list size). + - 1a1. System shows an error message that the index is invalid. + - Use case ends. + +### 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. +4. Should be able to handle up to 100 active (not archived) loans per contact without a noticeable sluggishness in + performance for typical + usage. +5. Returned loans should be archived instead of deleted for future reference. +6. The archived data should be stored for at least 3 years. +7. Should be able to support multiple user sessions with password authentication on the same device. +8. Archived data should be encrypted and only accessible by authorized users (admin and the user who created the data). +9. Loan values should be in a single currency (e.g. USD, SGD, EUR, etc.) and should be formatted as per the currency + standards. +10. Loan deadlines should not be more than 100 years from the date of loan creation. -------------------------------------------------------------------------------------------------------------------- @@ -343,40 +684,216 @@ 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 …​ }_ ### Deleting a person 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 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. + +### Linking a loan + +1. Linking a loan to a contact + + 1. Prerequisites: At least one contact in the list. + + 1. Test case: `linkloan 1 v/100 s/2021-10-10 r/2021-10-20`
+ Expected: A loan of value 100 is linked to the first contact in current view. Details of the linked loan shown in the status + message. Timestamp in the status bar is updated. Note that if there are no contacts in view this command will not work. + Perform `list` command if necessary. + +### Viewing loans +1. Viewing loans of a contact + + 1. Prerequisites: At least one contact in the list. + + 1. Test case: `viewloan 1`
+ Expected: All unreturned loans linked to the first contact in the current view are shown. + Note that if there are no contacts in view this command will not work. Perform `list` command if necessary. + 1. Test case: `viewloan -a 1`
+ Expected: All loans linked to the first contact in the current view are shown. + Here, the `-a` flag means all. + Note that if there are no contacts in view this command will not work. Perform `list` command if necessary. + +### Marking and unmarking a loan + +1. Marking a loan as returned + + 1. Prerequisites: At least one loan linked. + + 1. Test case: `viewloan -a`, followed by `markloan 1`
+ Expected: The first loan is marked as returned. Details of the marked loan shown in the status message. + +2. Unmarking a loan (i.e. marking it as not returned) + 1. Prerequisites: At least one loan linked. + + 1. Test case: `viewloan -a`, followed by `unmarkloan 1`
+ Expected: The first loan is marked as not returned. Details of the unmarked loan shown in the status message. - 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. +### Editing a loan - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. +1. Editing a loan - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. + 1. Prerequisites: At least one loan linked. -1. _{ more test cases …​ }_ + 1. Test case: `viewloan -a`, followed by `editloan 1 v/200`
+ Expected: The value of the first loan is updated to 200. Details of the edited loan shown in the status message. + +### Deleting a loan + +1. Deleting a loan + + 1. Prerequisites: At least one loan linked. + + 1. Test case: `viewloan -a`, followed by `deleteloan 1`
+ Expected: The first loan is deleted. Details of the deleted loan shown in the status message. + +### Analytics Command +1. Viewing analytics of a contact + + 1. Prerequisites: At least one contact in the list. + + 1. Test case: `analytics 1`
+ Expected: The analytics of the first contact in the current view are shown. + Note that if there are no contacts in view this command will not work. Perform `list` command if necessary. ### Saving data 1. Dealing with missing/corrupted data files + 1. Close the app. Choose either to simulate a missing data file or corrupted data file, but not both. + + 1. _To simulate a missing file, go to ./data and delete the JSON file inside, where . refers to + th directory containing the jar file._ + 2. _To simulate a corrupted file, open the JSON file and delete a few characters from the middle of the file._ + + 1. Launch the app.
+ Expected: The app should launch successfully. A new JSON file should be created in the + ./data folder. For a missing file, the address book should show the sample data. + For a corrupted file, a blank address book should be shown. + 2. After populating the address book with some data, repeat steps i to iv for the other of missing/corrupted. + +2. After performing several operations that changes the data (e.g. linkloan, add a person, etc.), + ensure that closing and re-opening the app retains the changed data. + +### Exiting the app + +You can exit the app in the following ways: +1. Click the close button on the window title bar.
+ +2. Enter `exit` into the GUI. - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ +In either case, the app should close. + +-------------------------------------------------------------------------------------------------------------------- + +## **Appendix: Effort** + +The main effort for this project was spent on creating the loan management features, which were not present in AB3. +These include: +* linking a loan +* viewing loans +* marking and unmarking a loan +* deleting loans +* editing loans +* viewing analytics of a contact + +Much inspiration was drawn from the existing commands in AB3, as well as the tutorial to add a new command. + +While the first five features looked similar, some required more effort than the others. +The main difficulty we faced include how to implement the deletion and editing of loans. +We had to ensure deletion can only happen if that loan is currently within view, else there could +easily be mistakes. Likewise for editing a loan. The solution we came up with was to alter the +person and loan lists in view, based on the commands given. Based on the lists in view, we decide +if each operation can be done. + +The analytics feature was the most challenging feature to implement. This is because we needed to define +the analytics that we wanted to show, and then implement the logic to calculate these analytics. The GUI, +in particular the pie chart, was also challenging to implement. + +Nonetheless, we managed to implement all the features we set out to do, and we are proud of the final product. +In particular, we are proud of the analytics feature, which we believe is a unique feature that sets our app apart. + +-------------------------------------------------------------------------------------------------------------------- + +## **Appendix: Planned Enhancements** + +Team size: 5 + +1. After executing `viewloan`, if we call `viewloan 1`, the error message provided states "The person index is invalid". + A better error message would be something like "Please run the list command before running this command again". +2. When entering an email for a new person in the form of `name@domain`(e.g. `jameshoexample@com`), an error message should be displayed and + the new person shouldn't be added, as opposed to the current behaviour. This is because emails are typically + in the form of `local-part@mail-server.domain`(`jameshoexample@gmail.com`). +3. Detect duplicate names, including case-insensitive ones. For example, if we have a person named "John Doe", + we should not be able to add another person named "john doe". +4. Do not allow the `/` character inside any field when adding a new person, since it is a special character for prefixes. +5. Error message for the `linkloan` command should be more specific to the error, e.g. different error messages for +incorrect date format and a start date before end date. +6. All fields should have a minimum length of 1 character and maximum length of 500 characters. +Otherwise, an error message should be displayed, e.g. for name, "Name cannot be empty" or +"Name is cannot exceed 500 characters". Similar for other fields. +7. Error messages related to indices should be more specific to the error. +For example, if the user enters `viewloan 0`, the error message should be something like "INDEX must be a positive integer". +If the user enters `viewloan 8` when there are only 7 contacts, the error message should be something like "INDEX must be between 1 and 7". +8. Reject loans that are > 2 decimal places as invalid. +For those loans that are < 2 decimal places, change them to 2 decimal places +format when displaying them instead of showing their exact value. + +-------------------------------------------------------------------------------------------------------------------- -1. _{ more test cases …​ }_ +## Glossary + +Order is roughly according to when they first appear in the guide. + +* **Architecture Diagram**: A diagram that shows how the different components interact with each + other at a high level. +* **Sequence Diagram**: A diagram that shows how the different components interact with each other + when a particular command is executed. +* **API**: Application Programming Interface, a set of rules that allows different software applications + to communicate with each other to form an entire system. +* **UI**: User Interface. +* **OOP**: Object-Oriented Programming, a programming paradigm based on the concept of "objects", + which can contain data and code: data in the form of fields, and code in the form of procedures. + The objects interact with each other. +* **Class**: Classes are used to create and define objects. A feature of OOP. +* **JSON**: JavaScript Object Notation, a lightweight data-interchange format. Files of this format + are used to store loan data on the hard disk. +* **Data archiving**: The process of moving data that is no longer actively used to a separate storage. +* **CLI**: Command Line Interface. +* **GUI**: Graphical User Interface. +* **User stories**: A user story is an informal, general explanation of a software feature written from the + perspective of the end user. +* **Cash**: Money in the form of coins or notes, as opposed to cheques or credit. *All loans in this project + are in cash, rather than items*. For consistency, we will avoid using the term "money" in this guide. +* **Currency**: Money of a certain country(e.g. USD, SGD, EUR for United States Dollars, SinGapore Dollars, + and EURos respectively). +* **Use cases**: A specific situation in which a product or service could potentially be used. +* **Actor**: A person or thing that performs an action. +* **MSS**: Main Success Scenario, the most common path through a use case. +* **Extensions**: The alternative paths through a use case. +* **Non-Functional Requirements**: A requirement that specifies criteria that can be used to judge the operation of + a system, rather than specific behaviours. +* **Mainstream OS**: Windows, Linux, Unix, or MacOS. +* **Jar file**: A Java Archive file, used to distribute a set of Java classes or applications as a single file. diff --git a/docs/Documentation.md b/docs/Documentation.md index 3e68ea364e7..61fc35a7419 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -10,7 +10,7 @@ title: Documentation guide * To learn how set it up and maintain the project website, follow the guide [_[se-edu/guides] **Using Jekyll for project documentation**_](https://se-education.org/guides/tutorials/jekyll.html). * Note these points when adapting the documentation to a different project/product: * The 'Site-wide settings' section of the page linked above has information on how to update site-wide elements such as the top navigation bar. - * :bulb: In addition to updating content files, you might have to update the config files `docs\_config.yml` and `docs\_sass\minima\_base.scss` (which contains a reference to `AB-3` that comes into play when converting documentation pages to PDF format). + * :bulb: In addition to updating content files, you might have to update the config files `docs\_config.yml` and `docs\_sass\minima\_base.scss` (which contains a reference to `LoanGuard Pro` that comes into play when converting documentation pages to PDF format). * If you are using Intellij for editing documentation files, you can consider enabling 'soft wrapping' for `*.md` files, as explained in [_[se-edu/guides] **Intellij IDEA: Useful settings**_](https://se-education.org/guides/tutorials/intellijUsefulSettings.html#enabling-soft-wrapping) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 7abd1984218..52ba940a5e1 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,10 +3,60 @@ layout: page title: User Guide --- -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. +## Table of Contents + +[1. Introduction](#introduction)
+[2. Quick Start](#quick-start)
+[3. Command Summary](#command-summary)
+[4. Features Description](#features-description)
+ +- [4.1 Contact Management Features](#contact-management-features)
+ - [Adding a person: `add`](#adding-a-person-add)
+ - [Listing all persons: `list`](#listing-all-persons--list)
+ - [Editing a person: `edit`](#editing-a-person--edit)
+ - [Locating persons by name: `find`](#locating-persons-by-name-find)
+ - [Deleting a person: `delete`](#deleting-a-person--delete)
+ - [Clearing all entries: `clear`](#clearing-all-entries--clear)
+- [4.2 Basic Loan Management Features](#basic-loan-management-features)
+ - [Adding a loan: `linkloan`](#adding-a-loan-linkloan)
+ - [Viewing loans of a person: `viewloan`](#viewing-loans-of-a-person-viewloan)
+ - [Mark/Unmark a loan as returned: `markloan/unmarkloan`](#markunmark-a-loan-as-returned-markloanunmarkloan)
+ - [Editing a loan: `editloan`](#editing-a-loan-editloan)
+ - [Deleting a loan: `deleteloan`](#deleting-a-loan-deleteloan)
+- [4.3 Advanced Loan Management Features](#advanced-loan-management-features)
+ - [Analysing a client's loan records: `analytics`](#analysing-a-clients-loan-records-analytics)
+- [4.4 Miscellaneous Features](#miscellaneous-features)
+ - [Viewing help: `help`](#viewing-help--help)
+ - [Exiting the program: `exit`](#exiting-the-program--exit)
+ - [Saving the data](#saving-the-data)
+ - [Editing the data file](#editing-the-data-file)
+ +[5. FAQ](#faq)
+[6. Known Issues](#known-issues)
-* Table of Contents -{:toc} +-------------------------------------------------------------------------------------------------------------------- + +## Introduction + +LoanGuardPro 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 are a moneylender looking to **keep track of your clients' contacts and loans**, LoanGuardPro is the right tool for you. + +It is in the form of an address book and supports basic contact and loan handling features like adding, editing, deleting, and viewing contacts and loans. +More advanced features like analysing a client's loaning history are also available. + +### How to Use this User Guide + +* If you are new to Command Line Interfaces (CLI), go to + this [website](https://www.theodinproject.com/lessons/foundations-command-line-basics) to learn the basics. +* If you are new to LoanGuardPro, go to the [Quick Start](#quick-start) section to download and set up the application. +* If you are looking for detailed explanations of contact management features, refer to + the [Contact Management Features](#contact-management-features) section. +* If you are looking for detailed explanations of basic loan management features, refer to + the [Basic Loan Management Features](#basic-loan-management-features) section. +* If you are looking for detailed explanations of advanced loan management features, refer to + the [Advanced Loan Management Features](#advanced-loan-management-features) section. +* If you encounter any issues, refer to the [Known issues](#known-issues) section. -------------------------------------------------------------------------------------------------------------------- @@ -14,32 +64,32 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo 1. Ensure you have Java `11` or above installed in your Computer. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +1. Download the latest `loanguardpro.jar` from [here](https://github.com/AY2324S2-CS2103T-W13-1/tp/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +1. Copy the file to the folder you want to use as the _home folder_ for your LoanGuardPro. -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.
+1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar loanguardpro.jar` + command to run the application.
A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
![Ui](images/Ui.png) -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
+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: - * `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. + * `viewloan 1` : View all active loans of the 1st person shown in the current list. + * `linkloan 2 v/500.00 s/2024-02-15 r/2025-02-15` : Link a loan of $500.00 to the 2nd person shown in the current + list with a start date of 15th Feb 2024 and repayment date of 15th Feb 2025. - * `clear` : Deletes all contacts. + * `viewloan` : View all active loans. - * `exit` : Exits the app. +1. Refer to the [Command Summary](#command-summary) section for details of the commands available. -1. Refer to the [Features](#features) below for details of each command. + -------------------------------------------------------------------------------------------------------------------- -## Features +## Command summary
@@ -57,20 +107,66 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo * 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.
+* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be + ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. -* If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. +* 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` +There are three main categories of commands: Contact Management, Basic Loan Management, and Advanced Loan Management. -Shows a message explaning how to access the help page. +### Contact Management -![help message](images/helpMessage.png) +| 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` | -Format: `help` +### Basic Loan Management + +| Action | Format, Examples | +|-----------------|---------------------------------------------------------------------------------------------------------------------| +| **Link Loan** | `linkloan INDEX v/VALUE s/START_DATE r/RETURN_DATE`
e.g., `linkloan 1 v/500.00 s/2024-02-15 r/2025-02-15` | +| **View Loan** | `viewloan [FLAG] [INDEX]`
e.g., `viewloan 1`, `viewloan -a 1` | +| **Mark Loan** | `markloan INDEX`
e.g., `markloan 1` | +| **Unmark Loan** | `unmarkloan INDEX`
e.g., `unmarkloan 1` | +| **Edit Loan** | `editloan INDEX [v/VALUE] [s/START_DATE] [r/RETURN_DATE]`
e.g., `editloan 1 v/600.00 s/2024-02-15 r/2025-02-15` | +| **Delete Loan** | `deleteloan INDEX`
e.g., `deleteloan 1` | + +### Advanced Loan Management + +| Action | Format, Examples | +|---------------|-------------------------------------------| +| **Analytics** | `analytics INDEX`
e.g., `analytics 1` | + + +### Miscellaneous + +| Action | Format | +|----------|--------| +| **Exit** | `exit` | +| **Help** | `help` | + +-------------------------------------------------------------------------------------------------------------------- + +## Features Description + +This section provides a detailed description of the features available in LoanGuardPro. There are three main categories +of features: + +* [Contact Management](#contact-management-features) +* [Basic Loan Management](#basic-loan-management-features) +* [Advanced Loan Management](#advanced-loan-management-features) + +## Contact Management Features ### Adding a person: `add` @@ -78,11 +174,22 @@ Adds a person to the address book. Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` -
:bulb: **Tip:** +:bulb: **Tip:** A person can have any number of tags (including 0) -
+ +Parameters Restrictions: + +* The name must only contain alphanumeric characters and spaces, and it should not be blank. The name is case-sensitive. +* The phone number must only contain numbers. +* The email must be in the format `local-part@domain`. + +Expected Behaviour: + +* A success message in the form of "New person added: [person details]" will be shown. +* The person will be added to the address book and will be shown in the person list. 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` @@ -92,22 +199,40 @@ Shows a list of all persons in the address book. Format: `list` +Expected Behaviour: + +* A list of all persons in the address book will be shown. + ### Editing a person : `edit` Edits an existing person in the address book. Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ +Parameters Restrictions: + * At least one of the optional fields must be provided. +* The index **must be a positive integer** 1, 2, 3, …​, and it must not exceed the number of persons shown in the list. +* The name must only contain alphanumeric characters and spaces, and it should not be blank. The name is case-sensitive. +* The phone number must only contain numbers. +* The email must be in the format `local-part@domain`. + +Expected Behaviour: + * 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. + specifying any tags after it. +* A success message in the form of "Edited Person: [person details]" will be shown. +* The person will be updated in the address book and will be shown in the person list. Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. + +* `edit 1 p/91234567 e/johndoe@example.com` + * Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` + respectively. +* `edit 2 n/Betsy Crower t/` + * Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. ### Locating persons by name: `find` @@ -115,14 +240,23 @@ Finds persons whose names contain any of the given keywords. Format: `find KEYWORD [MORE_KEYWORDS]` -* 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` +Parameters Restrictions: + +* At least one keyword must be provided. +* The keywords are case-insensitive. +* The order of the keywords does not matter. + +Expected Behaviour: + * 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` +* A list of persons whose names contain the given keywords will be shown. +* See the example below for more concrete details. Examples: + * `find John` returns `john` and `John Doe` * `find alex david` returns `Alex Yeoh`, `David Li`
![result for 'find alex david'](images/findAlexDavidResult.png) @@ -133,11 +267,18 @@ Deletes the specified person from the address book. Format: `delete INDEX` -* Deletes the person at the specified `INDEX`. +Parameters Restrictions: + * The index refers to the index number shown in the displayed person list. * The index **must be a positive integer** 1, 2, 3, …​ +Expected Behaviour: + +* A success message in the form of "Deleted Person: [person details]" will be shown. +* The person will be removed from the address book and will no longer be shown in the person list. + 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. @@ -147,6 +288,187 @@ Clears all entries from the address book. Format: `clear` +Expected Behaviour: + +* A success message in the form of "Address book has been cleared!" will be shown. +* The address book will be empty. + + + +-------------------------------------------------------------------------------------------------------------------- + +## Basic Loan Management Features + +### Adding a loan: `linkloan` + +Links a loan to a person in the address book. + +:information_source: The word `linkloan` is used to distinguish between the `add` command for adding a person and +the `linkloan` command for linking a loan to a person. + +Format: `linkloan INDEX v/VALUE s/START_DATE r/RETURN_DATE` + +Parameters Restrictions: + +* Links a loan to 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, …​, and it must not exceed the number of persons shown in the list. +* The loan value must be a positive float value that is **at most 2 decimal places**, as the app behavior may not be optimal for any higher precision. +* The start date and return date must be in the format `YYYY-MM-DD`. +* The return date must be after the start date. +* Year value has to be below 9999. + +:bulb: **Tip:** +If you are on a view page with no person contacts, such as the "view all loans" page or the "analytics" page, you can use the `list` command to go back to the person list. That will allow you to use the `linkloan` command. + +Expected Behaviour: + +* A success message in the form of "New loan linked: [person name] [loan description]" will be shown. +* The loan can then be found in both the overall loan list and the loan list of that person. + +Example: `linkloan 1 v/500.00 s/2024-02-15 r/2025-02-15` + +* Links a loan of $500.00 to the person at the 1st index with a start date of 15th Feb 2024 and return date of 15th Feb + 2025. + +### Viewing loans of a person: `viewloan` + +Shows loans in the address book. + +Format: `viewloan [FLAG] [INDEX]` + +Parameters Restrictions: + +* The optional index refers to the index number shown in the displayed person list. The index **must be a positive + integer** 1, 2, 3, …​, and it must not exceed the number of persons shown in the list. +* If the optional index is not provided, all loans, across all clients in the list will be shown. +* The only optional flag is `-a` to show all loans including the inactive ones. + +:bulb: **Tip:** A loan is considered active if the loan has not been marked as returned. Otherwise, it is considered inactive. + +Expected Behaviour: + +* A success message of the form "Listed all loans associated with [person details]." will be shown. +* The list is ordered by the end date of the loan. +* Only the active loans will be shown if the flag `-a` is not provided. If it is provided, both active and inactive loans will be shown. +* If the index is not provided, all loans across all clients in the list will be shown. +* If the index is provided, all loans of the person at the specified `INDEX` will be shown. + +Examples: `viewloan 1`, `viewloan -a 1` + +* The figure below shows an example of `viewloan 1`(left) and `viewloan -a 1`(right) being executed. + +![viewloan](images/viewloan.png) + +### Mark/Unmark a loan as returned: `markloan/unmarkloan` + +Marks or unmarks a loan as returned. + +Format: `markloan INDEX`, `unmarkloan INDEX` + +Parameters Restrictions: +* The index refers to the index number shown in the displayed loan list. The index **must be a positive integer** 1, 2, 3, …​, and it must not exceed the number of loans shown in the list. + +Expected Behaviour: +* A success message in the form of "Loan marked: [loan details]" or "Loan unmarked: [loan details]" will be shown. +* The status of the loan will be updated accordingly and will be reflected in the loan list. + +Examples: `markloan 1`, `unmarkloan 1` + +* Marks or unmarks the loan at that is in the 1st position in the loan list. + +### Editing a loan: `editloan` + +Edits an existing loan in the address book. + +Format: `editloan INDEX [v/VALUE] [s/START_DATE] [r/RETURN_DATE]` + +Parameters Restrictions: + +* The index refers to the index number shown in the displayed loan list. The index **must be a positive integer** 1, 2, 3, …​, and it must not exceed the number of loans shown in the list. +* The loan value must be a positive float value that is **at most 2 decimal places**, as the app behavior may not be optimal for any higher precision. +* The start date and return date must be in the format `YYYY-MM-DD`. +* The return date must be after the start date. +* Year value has to be below 9999. +* The loan value, start date and return date are all optional parameters, but at least one of them must be provided. + +Expected Behaviour: + +* A success message in the form of "Loan edited: [loan details]" will be shown. +* The loan will be updated in the loan list. + +Examples: + +* `editloan 1 v/600.00 s/2024-02-15 r/2025-02-15` + * Edits the loan at the 1st position in the loan list to have a value of $600.00, a start date of 15th Feb 2024, and a + return date of 15th Feb 2025. +* `editloan 3 s/2021-01-01` + * Edits the loan at the 3rd position in the loan list to have a start date of 1st Jan 2021. + +### Deleting a loan: `deleteloan` + +Deletes a loan permanently from the address book. + +Format: `deleteloan INDEX` + +Parameters Restrictions: + +* The index refers to the index number shown in the displayed loan list. +The index **must be a positive integer** 1, 2, 3, …​, and it must not exceed the number of loans shown in the list. + +Expected Behaviour: + +* A success message in the form of "Loan deleted: [loan details]" will be shown. +* The loan will be removed from the loan list. + +Example: `deleteloan 1` + +* Deletes the loan at the 1st position in the loan list. + + + +-------------------------------------------------------------------------------------------------------------------- + +## Advanced Loan Management Features + +### Analysing a client's loan records: `analytics` + +Provides visual analytics of a client's loan records based on three indices: Reliability, Impact, and Urgency. +* The Reliability index is defined as the ratio of overdue loans to the total number of loans. +* The Impact index is defined as the ratio of the average loan value to the maximum loan value. +* The Urgency index is defined as follows: + * Define URGENCY_ALL as the number of days from the current date to the earliest return date among **all** active loans. + * Define URGENCY_CLIENT as the number of days from the current date to the earliest return date among **this particular client's** active loans. + * The Urgency index is equal to the ratio of URGENCY_ALL to URGENCY_CLIENT. + * The computation will only consider loans that are not overdue. +* These indexes are then converted in percentage form and visualized in a pie chart. + +Format: `analytics INDEX` + +Parameters Restrictions: + +* The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​, and it must not exceed the number of persons shown in the list. + +Expected Behaviour: + +* A success message in the form of "Analytics generated for [person name]" will be shown. +* A visual representation of the client's loan records will be shown. + +Example: `analytics 1` + +![result analytics](images/analytics.png) + +-------------------------------------------------------------------------------------------------------------------- + +## Miscellaneous Features + +### Viewing help : `help` + +Shows a message explaining how to access the help page. + +![help message](images/helpMessage.png) + +Format: `help` + ### Exiting the program : `exit` Exits the program. @@ -155,44 +477,38 @@ Format: `exit` ### Saving the data -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +LoanGuardPro data are saved in the hard disk automatically after any command that changes the data. There is no need to +save manually. ### Editing the data file -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. - -
: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. -
+LoanGuardPro 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. -### Archiving data files `[coming in v2.0]` +:exclamation: **Caution:** +If your changes to the data file makes its format invalid, LoanGuardPro **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 LoanGuardPro 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. -_Details coming soon ..._ + -------------------------------------------------------------------------------------------------------------------- ## 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 LoanGuardPro home folder. -------------------------------------------------------------------------------------------------------------------- ## 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. +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. --------------------------------------------------------------------------------------------------------------------- + -## Command summary - -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +-------------------------------------------------------------------------------------------------------------------- diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..e48c82958e1 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "LoanGuard Pro" theme: minima header_pages: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..7fb4e63a7cb 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -288,7 +288,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "LoanGuard Pro"; font-size: 32px; } } diff --git a/docs/diagrams/AnalyticsSequenceDiagram.puml b/docs/diagrams/AnalyticsSequenceDiagram.puml new file mode 100644 index 00000000000..5d350b1360c --- /dev/null +++ b/docs/diagrams/AnalyticsSequenceDiagram.puml @@ -0,0 +1,33 @@ + +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":AnalyticsCommand" as AnalyticsCommand LOGIC_COLOR +end box + +box Person MODEL_COLOR_T1 +participant ":Analytics" as Analytics MODEL_COLOR +end box + +[-> AnalyticsCommand : execute(model) +activate AnalyticsCommand + +AnalyticsCommand -> Analytics : getAnalytics(ObservableList) +activate Analytics + +loop LoanRecords.size() times +Analytics -> Analytics :updateNumFields(Loan) +Analytics -> Analytics :updateValueFields(Loan) +Analytics -> Analytics :updateDateFields(Loan) +end +Analytics -> Analytics :updatePropFields(Loan) +Analytics -> Analytics :updateAverageFields(Loan) +Analytics --> AnalyticsCommand : +deactivate Analytics + +[<-- AnalyticsCommand +deactivate AnalyticsCommand + +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..9a9b0575890 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -6,11 +6,13 @@ skinparam classBackgroundColor MODEL_COLOR AddressBook *-right-> "1" UniquePersonList AddressBook *-right-> "1" UniqueTagList +AddressBook *-right-> "1" UniqueLoanList UniqueTagList -[hidden]down- UniquePersonList UniqueTagList -[hidden]down- UniquePersonList UniqueTagList -right-> "*" Tag UniquePersonList -right-> Person +UniqueLoanList --> Loan Person -up-> "*" Tag @@ -18,4 +20,6 @@ Person *--> Name Person *--> Phone Person *--> Email Person *--> Address + +Loan *--> Person @enduml diff --git a/docs/diagrams/DeleteLoanSequenceDiagram.puml b/docs/diagrams/DeleteLoanSequenceDiagram.puml new file mode 100644 index 00000000000..a063de26ba0 --- /dev/null +++ b/docs/diagrams/DeleteLoanSequenceDiagram.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 ":DeleteLoanCommandParser" as DeleteLoanCommandParser LOGIC_COLOR +participant "<< class >>\nParserUtil" as ParserUtil LOGIC_COLOR +participant "d:DeleteLoanCommand" as DeleteLoanCommand 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("deleteloan 1") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("deleteloan 1") +activate AddressBookParser + +create DeleteLoanCommandParser +AddressBookParser -> DeleteLoanCommandParser +activate DeleteLoanCommandParser + +DeleteLoanCommandParser --> AddressBookParser +deactivate DeleteLoanCommandParser + +AddressBookParser -> DeleteLoanCommandParser : parse("1") +activate DeleteLoanCommandParser + +DeleteLoanCommandParser -> ParserUtil : parseIndex("1") +activate ParserUtil + +ParserUtil --> DeleteLoanCommandParser +deactivate ParserUtil + + +create DeleteLoanCommand +DeleteLoanCommandParser -> DeleteLoanCommand +activate DeleteLoanCommand + +DeleteLoanCommand --> DeleteLoanCommandParser : +deactivate DeleteLoanCommand + +DeleteLoanCommandParser --> AddressBookParser : +deactivate DeleteLoanCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +DeleteLoanCommandParser -[hidden]-> AddressBookParser +destroy DeleteLoanCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> DeleteLoanCommand : execute(model) +activate DeleteLoanCommand + +DeleteLoanCommand -> Model : getSortedLoanList() +activate Model + +Model --> DeleteLoanCommand +deactivate Model + +create CommandResult +DeleteLoanCommand -> CommandResult +activate CommandResult + +CommandResult --> DeleteLoanCommand +deactivate CommandResult + +DeleteLoanCommand --> LogicManager : +deactivate DeleteLoanCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/LinkLoanSequenceDiagram.puml b/docs/diagrams/LinkLoanSequenceDiagram.puml new file mode 100644 index 00000000000..00ca7719449 --- /dev/null +++ b/docs/diagrams/LinkLoanSequenceDiagram.puml @@ -0,0 +1,83 @@ +@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 ":LinkLoanCommandParser" as LinkLoanCommandParser LOGIC_COLOR +participant "<< class >>\nParserUtil" as ParserUtil LOGIC_COLOR +participant "l:LinkLoanCommand" as LinkLoanCommand 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("linkloan ...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("linkloan ...") +activate AddressBookParser + +create LinkLoanCommandParser +AddressBookParser -> LinkLoanCommandParser +activate LinkLoanCommandParser + +LinkLoanCommandParser --> AddressBookParser +deactivate LinkLoanCommandParser + +AddressBookParser -> LinkLoanCommandParser : parse(...) +activate LinkLoanCommandParser + +LinkLoanCommandParser -> ParserUtil : parseIndex(...) +activate ParserUtil + +ParserUtil --> LinkLoanCommandParser : +deactivate ParserUtil + +LinkLoanCommandParser -> ParserUtil : parseLoan(...) +activate ParserUtil + +ParserUtil --> LinkLoanCommandParser : +deactivate ParserUtil + +create LinkLoanCommand +LinkLoanCommandParser -> LinkLoanCommand +activate LinkLoanCommand + +LinkLoanCommand --> LinkLoanCommandParser : +deactivate LinkLoanCommand + +LinkLoanCommandParser --> AddressBookParser : +deactivate LinkLoanCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +LinkLoanCommandParser -[hidden]-> AddressBookParser +destroy LinkLoanCommandParser + +AddressBookParser --> LogicManager : +deactivate AddressBookParser + +LogicManager -> LinkLoanCommand : execute(m) +activate LinkLoanCommand + +LinkLoanCommand -> Model : getFilteredPersonList() +activate Model + +Model --> LinkLoanCommand +deactivate Model + +create CommandResult +LinkLoanCommand -> CommandResult +activate CommandResult + +CommandResult --> LinkLoanCommand +deactivate CommandResult + +LinkLoanCommand --> LogicManager : +deactivate LinkLoanCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..48aff47fb4c 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -13,12 +13,15 @@ Class ModelManager Class UserPrefs Class UniquePersonList +Class UniqueLoanList Class Person -Class Address -Class Email Class Name Class Phone +Class Email +Class Address Class Tag +Class Loan + Class I #FFFFFF } @@ -36,13 +39,17 @@ ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList +AddressBook *--> "1" UniqueLoanList UniquePersonList --> "~* all" Person +UniqueLoanList o-->"~* all" Loan Person *--> Name Person *--> Phone Person *--> Email Person *--> Address Person *--> "*" Tag +Loan *--> "1" Person + Person -[hidden]up--> I UniquePersonList -[hidden]right-> I @@ -51,4 +58,5 @@ Phone -[hidden]right-> Address Address -[hidden]right-> Email ModelManager --> "~* filtered" Person +ModelManager --> "~* filtered" Loan @enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..521df432eeb 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -20,6 +20,8 @@ Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson Class JsonAdaptedTag +Class JsonAdaptedLoan +Class JsonAdaptedUniqueLoanList } } @@ -39,5 +41,7 @@ JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonSerializableAddressBook --> "1" JsonAdaptedUniqueLoanList +JsonAdaptedUniqueLoanList --> "*" JsonAdaptedLoan @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..c548c44a2cd 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -15,6 +15,9 @@ Class PersonListPanel Class PersonCard Class StatusBarFooter Class CommandBox +Class LoanListPanel +Class AnalyticsPanel +Class LoanCard } package Model <> { @@ -33,11 +36,15 @@ UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" LoanListPanel +MainWindow *-down-> "1" AnalyticsPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow PersonListPanel -down-> "*" PersonCard +LoanListPanel -down-> "*" LoanCard + MainWindow -left-|> UiPart ResultDisplay --|> UiPart @@ -45,13 +52,18 @@ CommandBox --|> UiPart PersonListPanel --|> UiPart PersonCard --|> UiPart StatusBarFooter --|> UiPart +LoanListPanel --|> UiPart +AnalyticsPanel --|> UiPart HelpWindow --|> UiPart PersonCard ..> Model +LoanCard ..> Model +AnalyticsPanel ..> Model UiManager -right-> Logic MainWindow -left-> Logic PersonListPanel -[hidden]left- HelpWindow +LoanListPanel -[hidden]left PersonListPanel HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay ResultDisplay -[hidden]left- StatusBarFooter diff --git a/docs/diagrams/ViewLoanSequenceDiagram.puml b/docs/diagrams/ViewLoanSequenceDiagram.puml new file mode 100644 index 00000000000..825da5e71a5 --- /dev/null +++ b/docs/diagrams/ViewLoanSequenceDiagram.puml @@ -0,0 +1,97 @@ +@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 ":ViewLoanCommandParser" as ViewLoanCommandParser LOGIC_COLOR +participant "d:ViewLoanCommand" as ViewLoanCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant "l:List" as FilteredList MODEL_COLOR +participant "p:Person" as Person MODEL_COLOR +participant "lr:LoanRecords" as LoanRecords MODEL_COLOR +end box + +[-> LogicManager : execute("viewloan 1") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("viewloan 1") +activate AddressBookParser + +create ViewLoanCommandParser +AddressBookParser -> ViewLoanCommandParser +activate ViewLoanCommandParser + +ViewLoanCommandParser --> AddressBookParser +deactivate ViewLoanCommandParser + +AddressBookParser -> ViewLoanCommandParser : parse("1") +activate ViewLoanCommandParser + +create ViewLoanCommand +ViewLoanCommandParser -> ViewLoanCommand +activate ViewLoanCommand + +ViewLoanCommand --> ViewLoanCommandParser : +deactivate ViewLoanCommand + +ViewLoanCommandParser --> AddressBookParser : +deactivate ViewLoanCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ViewLoanCommandParser -[hidden]-> AddressBookParser +destroy ViewLoanCommandParser + +AddressBookParser --> LogicManager : +deactivate AddressBookParser + +LogicManager -> ViewLoanCommand : execute(m) +activate ViewLoanCommand + +ViewLoanCommand -> Model : getFilteredPersonList() +activate Model + +Model --> ViewLoanCommand : +deactivate Model + +ViewLoanCommand -> FilteredList : get(0) +activate FilteredList + +FilteredList --> ViewLoanCommand : +deactivate FilteredList + +ViewLoanCommand -> Person : getLoanRecords() +activate Person + +Person --> ViewLoanCommand : +deactivate Person + +ViewLoanCommand -> LoanRecords : getLoanList() +activate LoanRecords + +LoanRecords --> ViewLoanCommand : +deactivate LoanRecords + +ViewLoanCommand -> Model : setLoanList(ll) +activate Model + +Model --> ViewLoanCommand +deactivate Model + +create CommandResult +ViewLoanCommand -> CommandResult +activate CommandResult + +CommandResult --> ViewLoanCommand +deactivate CommandResult + +ViewLoanCommand --> LogicManager : +deactivate ViewLoanCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/images/AnalyticsSequenceDiagram.png b/docs/images/AnalyticsSequenceDiagram.png new file mode 100644 index 00000000000..e78b30f740f Binary files /dev/null and b/docs/images/AnalyticsSequenceDiagram.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..5790ded9b38 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/DeleteLoanSequenceDiagram.png b/docs/images/DeleteLoanSequenceDiagram.png new file mode 100644 index 00000000000..7a5e4782df7 Binary files /dev/null and b/docs/images/DeleteLoanSequenceDiagram.png differ diff --git a/docs/images/LinkLoanSequenceDiagram.png b/docs/images/LinkLoanSequenceDiagram.png new file mode 100644 index 00000000000..cd4b701ffee Binary files /dev/null and b/docs/images/LinkLoanSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..16ab1a6f4ab 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..e2eb7cf1cb9 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..b7e58730f36 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 11f06d68671..eaa5aa72aef 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/ViewLoanSequenceDiagram.png b/docs/images/ViewLoanSequenceDiagram.png new file mode 100644 index 00000000000..55f4ee61437 Binary files /dev/null and b/docs/images/ViewLoanSequenceDiagram.png differ diff --git a/docs/images/analytics.png b/docs/images/analytics.png new file mode 100644 index 00000000000..4eb01e03efa Binary files /dev/null and b/docs/images/analytics.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..e67dc61f4f0 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/joseph31416.png b/docs/images/joseph31416.png new file mode 100644 index 00000000000..fa2aed5b33f Binary files /dev/null and b/docs/images/joseph31416.png differ diff --git a/docs/images/kjw142857.png b/docs/images/kjw142857.png new file mode 100644 index 00000000000..9376728d24e Binary files /dev/null and b/docs/images/kjw142857.png differ diff --git a/docs/images/marcus-ny.png b/docs/images/marcus-ny.png new file mode 100644 index 00000000000..a5861339987 Binary files /dev/null and b/docs/images/marcus-ny.png differ diff --git a/docs/images/narwhalsilent.png b/docs/images/narwhalsilent.png new file mode 100644 index 00000000000..ad10e9647c6 Binary files /dev/null and b/docs/images/narwhalsilent.png differ diff --git a/docs/images/viewloan.png b/docs/images/viewloan.png new file mode 100644 index 00000000000..b8ad1f1abee Binary files /dev/null and b/docs/images/viewloan.png differ diff --git a/docs/images/xiaorui-ui.png b/docs/images/xiaorui-ui.png new file mode 100644 index 00000000000..b9412931476 Binary files /dev/null and b/docs/images/xiaorui-ui.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..4e196b79900 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,18 @@ --- layout: page -title: AddressBook Level-3 +title: LoanGuard Pro --- -[![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-W13-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S2-CS2103T-W13-1/tp/actions) +[![codecov](https://codecov.io/gh/AY2324S2-CS2103T-W13-1/tp/graph/badge.svg?token=MZAGQZIFNS)](https://codecov.io/gh/AY2324S2-CS2103T-W13-1/tp) -![Ui](images/Ui.png) +![Ui](images/viewloan.png) +**LoanGuard Pro 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). +It supports basic contact and loan handling features like adding, editing, deleting, and viewing contacts and loans. More advanced features like analysing a client's loaning history are also available. -**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. +* If you are interested in using LoanGuard Pro, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing LoanGuard Pro, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..19e1dfa0bcd 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -61,6 +61,7 @@ public void init() throws Exception { storage = new StorageManager(addressBookStorage, userPrefsStorage); model = initModelManager(storage, userPrefs); + model.updateFilteredLoanList(Model.PREDICATE_SHOW_NO_LOANS); logic = new LogicManager(model, storage); diff --git a/src/main/java/seedu/address/commons/util/DateUtil.java b/src/main/java/seedu/address/commons/util/DateUtil.java new file mode 100644 index 00000000000..3c856c4e694 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/DateUtil.java @@ -0,0 +1,56 @@ +package seedu.address.commons.util; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import seedu.address.commons.exceptions.IllegalValueException; + +/** + * A utility class to handle date parsing and formatting. + */ +public class DateUtil { + + public static final String DATE_FORMAT = "yyyy-MM-dd"; + private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat(DATE_FORMAT); + + /** + * Parses a date string into a Date object. + * + * @param date The date string to parse. + * @return The parsed Date object. + * @throws IllegalValueException If the date string is not in the correct format. + */ + public static Date parse(String date) throws IllegalValueException { + DATE_FORMATTER.setLenient(false); + try { + return DATE_FORMATTER.parse(date); + } catch (ParseException e) { + throw new IllegalValueException(DATE_FORMAT + " is the only supported date format."); + } + } + + /** + * Formats a Date object into a date string. + * + * @param date The Date object to format. + * @return The formatted date string. + */ + public static String format(Date date) { + return DATE_FORMATTER.format(date); + } + + /** + * Adds a number of days to a Date object. + * + * @param date The Date object to add days to. + * @param days The number of days to add. + * @return The new Date object. + */ + public static Date addDay(Date date, int days) { + long time = date.getTime(); + time += days * 24 * 60 * 60 * 1000; + return new Date(time); + } + +} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..69f80525834 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -2,13 +2,17 @@ import java.nio.file.Path; +import javafx.beans.property.ObjectProperty; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.analytics.DashboardData; +import seedu.address.model.person.Loan; import seedu.address.model.person.Person; +import seedu.address.model.tabindicator.TabIndicator; /** * API of the Logic component @@ -16,6 +20,7 @@ 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. @@ -30,7 +35,9 @@ 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(); /** @@ -47,4 +54,10 @@ public interface Logic { * Set the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); + + ObservableList getSortedLoanList(); + + ObjectProperty getAnalytics(); + + ObjectProperty getTabIndicator(); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..f926387dfb9 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -5,6 +5,7 @@ import java.nio.file.Path; import java.util.logging.Logger; +import javafx.beans.property.ObjectProperty; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; @@ -15,9 +16,13 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.analytics.DashboardData; +import seedu.address.model.person.Loan; import seedu.address.model.person.Person; +import seedu.address.model.tabindicator.TabIndicator; import seedu.address.storage.Storage; + /** * The main LogicManager of the app. */ @@ -85,4 +90,19 @@ public GuiSettings getGuiSettings() { public void setGuiSettings(GuiSettings guiSettings) { model.setGuiSettings(guiSettings); } + + @Override + public ObservableList getSortedLoanList() { + return model.getSortedLoanList(); + } + + @Override + public ObjectProperty getAnalytics() { + return model.getDashboardData(); + } + + @Override + public ObjectProperty getTabIndicator() { + return model.getTabIndicator(); + } } diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..4bfd0766b1f 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -45,6 +45,7 @@ public static String format(Person person) { .append(person.getAddress()) .append("; Tags: "); person.getTags().forEach(builder::append); + builder.append("\n"); return builder.toString(); } diff --git a/src/main/java/seedu/address/logic/commands/AnalyticsCommand.java b/src/main/java/seedu/address/logic/commands/AnalyticsCommand.java new file mode 100644 index 00000000000..5b842941548 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AnalyticsCommand.java @@ -0,0 +1,80 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Analytics; +import seedu.address.model.person.Person; + +/** + * Represents a command to view the loans analytics data associated with a contact. + */ +public class AnalyticsCommand extends Command { + public static final String COMMAND_WORD = "analytics"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Displays the analytics of the person " + + "identified by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_SUCCESS = "Analytics generated"; + private final Index targetIndex; + + /** + * @param targetIndex The index of the person to view analytics for. + */ + public AnalyticsCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + assert lastShownList != null; + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person targetPerson = lastShownList.get(targetIndex.getZeroBased()); + model.updateFilteredLoanList(loan -> loan.isAssignedTo(targetPerson)); + Analytics targetAnalytics = Analytics.getAnalytics(model.getSortedLoanList()); + + model.generateDashboardData(targetAnalytics); + model.setIsAnalyticsTab(true); + + return new CommandResult(MESSAGE_SUCCESS + " for " + targetPerson.getName() + " ", + false, false, false); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AnalyticsCommand)) { + return false; + } + + AnalyticsCommand otherViewLoanCommand = (AnalyticsCommand) other; + return targetIndex.equals(otherViewLoanCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..0b34acb7054 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -18,6 +18,7 @@ public class ClearCommand extends Command { public CommandResult execute(Model model) { requireNonNull(model); model.setAddressBook(new AddressBook()); + model.setToPersonTab(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 249b6072d0d..8b8f92f6960 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -13,19 +13,30 @@ 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; + private final boolean isLoanRelated; + /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, boolean isLoanRelated) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.isLoanRelated = isLoanRelated; + } + + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + this(feedbackToUser, showHelp, exit, false); } /** @@ -33,9 +44,10 @@ 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, false); } + public String getFeedbackToUser() { return feedbackToUser; } @@ -48,6 +60,10 @@ public boolean isExit() { return exit; } + public boolean isLoanRelated() { + return isLoanRelated; + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..bf87cfa8672 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -42,6 +42,7 @@ public CommandResult execute(Model model) throws CommandException { Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); model.deletePerson(personToDelete); + model.setToPersonTab(); return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); } diff --git a/src/main/java/seedu/address/logic/commands/DeleteLoanCommand.java b/src/main/java/seedu/address/logic/commands/DeleteLoanCommand.java new file mode 100644 index 00000000000..4d2ffc04282 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteLoanCommand.java @@ -0,0 +1,78 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Loan; + +/** + * Deletes a loan from the address book. + */ +public class DeleteLoanCommand extends Command { + public static final String COMMAND_WORD = "deleteloan"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Delete the loan number of current person in view. \n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1 "; + public static final String MESSAGE_SUCCESS = "Loan deleted.\n" + + "Loan: %1$s"; + public static final String MESSAGE_FAILURE_LOAN = "No loan has been found " + + "for loan number: %1$d"; + private final Index loanIndex; + + /** + * Creates a DeleteLoanCommand to delete the specified loan. + * @param loanIndex Index of the loan in the last shown loan list. + */ + public DeleteLoanCommand(Index loanIndex) { + requireAllNonNull(loanIndex); + this.loanIndex = loanIndex; + } + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getSortedLoanList(); + assert lastShownList != null; + if (loanIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(String.format(MESSAGE_FAILURE_LOAN, loanIndex.getOneBased())); + } + // delete specified loan number + Loan loanToRemove = lastShownList.get(loanIndex.getZeroBased()); + model.deleteLoan(loanToRemove); + return new CommandResult(generateSuccessMessage(loanToRemove), false, false , true); + } + + /** + * Generates a command execution success message after loan is deleted. + */ + private String generateSuccessMessage(Loan removedLoan) { + return String.format(MESSAGE_SUCCESS, removedLoan); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteLoanCommand)) { + return false; + } + + DeleteLoanCommand e = (DeleteLoanCommand) other; + return loanIndex.equals(e.loanIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("loanIndex", loanIndex) + .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..34661300c9c 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -6,7 +6,6 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import java.util.Collections; import java.util.HashSet; @@ -84,7 +83,10 @@ public CommandResult execute(Model model) throws CommandException { } model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + if (model.getTabIndicator().getValue().getIsLoansTab()) { + model.updateFilteredPersonList(person -> person.isSamePerson(editedPerson)); + model.updateFilteredLoanList(loan -> loan.isAssignedTo(editedPerson)); + } return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); } @@ -139,7 +141,8 @@ public static class EditPersonDescriptor { private Address address; private Set tags; - public EditPersonDescriptor() {} + public EditPersonDescriptor() { + } /** * Copy constructor. @@ -209,6 +212,7 @@ public Optional> getTags() { return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/logic/commands/EditLoanCommand.java b/src/main/java/seedu/address/logic/commands/EditLoanCommand.java new file mode 100644 index 00000000000..219e4a5fa78 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditLoanCommand.java @@ -0,0 +1,213 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_RETURN_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_VALUE; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.LinkLoanCommand.LinkLoanDescriptor; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Loan; +import seedu.address.model.person.Person; + +/** + * Edits a loan of a person in the address book. + */ +public class EditLoanCommand extends Command { + + public static final String COMMAND_WORD = "editloan"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the loan specified " + + "by its loan index number, possibly changing " + + "its value, start date and/or return date. " + + "At least one field must be changed.\n" + + "Parameters: " + + "LOAN_INDEX " + + "[" + PREFIX_VALUE + "VALUE] " + + "[" + PREFIX_START_DATE + "START_DATE] " + + "[" + PREFIX_RETURN_DATE + "RETURN_DATE]\n" + + "INDEX must be a positive integer.\n" + + "Example: " + COMMAND_WORD + " " + + "5 " + + PREFIX_VALUE + "500.00 " + + PREFIX_START_DATE + "2024-02-15 " + + PREFIX_RETURN_DATE + "2024-04-21\n" + + "This edits the loan at loan index 5 " + + "to reflect a value of $500, " + + "a start date of 15 Feb 2024 " + + "and an end date of 21 April 2024."; + + public static final String MESSAGE_SUCCESS = "Loan successfully edited: %1$s"; + + public static final String MESSAGE_FAILURE_LOAN = "No loan has been found " + + "for loan number: %1$d"; + + private final EditLoanDescriptor editedDetails; + + private final Index loanIndex; + + /** + * @param editedDetails New value(s) of the edited field(s) of the loan, as an EditLoanDescriptor + * @param loanIndex Index of the loan to be edited + */ + public EditLoanCommand(EditLoanDescriptor editedDetails, Index loanIndex) { + requireAllNonNull(editedDetails, loanIndex); + this.editedDetails = editedDetails; + this.loanIndex = loanIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getSortedLoanList(); + assert lastShownList != null; + + if (loanIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(String.format(MESSAGE_FAILURE_LOAN, loanIndex.getOneBased())); + } + + Loan loanToEdit = lastShownList.get(loanIndex.getZeroBased()); + Person linkedPerson = loanToEdit.getAssignee(); + // Generate updated details of the loan + LinkLoanDescriptor updatedLoanDetails = generateEditedLoanDetails(loanToEdit, editedDetails); + // Delete the old loan + model.deleteLoan(loanToEdit); + // Create and link a new loan using the updated loan details + Loan editedLoan = model.addLoan(updatedLoanDetails, linkedPerson); + + return new CommandResult(generateSuccessMessage(editedLoan), false, false, true); + } + + /** + * Generates a command execution success message after loan is edited. + */ + private String generateSuccessMessage(Loan editedLoan) { + return String.format(MESSAGE_SUCCESS, editedLoan); + } + + /** + * Generates the new loan details in the form of a LinkLoanDescriptor. + * + * @param loanToEdit The original loan that was edited. + * @param editedDetails The details of the loan that were changed. + * @return All details of the new loan, including the changed and unchanged details. + * @throws CommandException If the edited date(s) are invalid. + */ + private LinkLoanDescriptor generateEditedLoanDetails(Loan loanToEdit, EditLoanDescriptor editedDetails) + throws CommandException { + BigDecimal newValue = editedDetails.getValue().orElse(loanToEdit.getValue()); + Date newStartDate = editedDetails.getStartDate().orElse(loanToEdit.getStartDate()); + Date newReturnDate = editedDetails.getReturnDate().orElse(loanToEdit.getReturnDate()); + requireAllNonNull(newValue, newStartDate, newReturnDate); + if (!Loan.isValidDates(newStartDate, newReturnDate)) { + throw new CommandException(Loan.DATE_CONSTRAINTS); + } + return new LinkLoanDescriptor(newValue, newStartDate, newReturnDate); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditLoanCommand)) { + return false; + } + + EditLoanCommand otherEditLoanCommand = (EditLoanCommand) other; + return editedDetails.equals(otherEditLoanCommand.editedDetails) + && loanIndex.equals(otherEditLoanCommand.loanIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("loanIndex", loanIndex) + .add("editedDetails", editedDetails) + .toString(); + } + + /** + * Stores the details of the loan that is edited. + */ + public static class EditLoanDescriptor { + private BigDecimal value = null; + private Date startDate = null; + private Date returnDate = null; + + /** + * Creates an instance of this EditLoanDescriptor with empty fields. + */ + public EditLoanDescriptor() {} + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(value, startDate, returnDate); + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + public Optional getValue() { + return Optional.ofNullable(value); + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Optional getStartDate() { + return Optional.ofNullable(startDate); + } + + public void setReturnDate(Date returnDate) { + this.returnDate = returnDate; + } + + public Optional getReturnDate() { + return Optional.ofNullable(returnDate); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditLoanDescriptor)) { + return false; + } + + EditLoanDescriptor otherEditLoanDescriptor = (EditLoanDescriptor) other; + return Objects.equals(value, otherEditLoanDescriptor.value) + && Objects.equals(getStartDate(), otherEditLoanDescriptor.getStartDate()) + && Objects.equals(getReturnDate(), otherEditLoanDescriptor.getReturnDate()); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("value", value) + .add("startDate", startDate) + .add("returnDate", returnDate) + .toString(); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..acac9a21374 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -13,7 +13,7 @@ public class ExitCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, false); } } diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..7d82aa532c9 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -30,6 +30,7 @@ public FindCommand(NameContainsKeywordsPredicate predicate) { public CommandResult execute(Model model) { requireNonNull(model); model.updateFilteredPersonList(predicate); + model.setToPersonTab(); return new CommandResult( String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); } diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..07d26e2a23c 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -16,6 +16,6 @@ public class HelpCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult(SHOWING_HELP_MESSAGE, true, false, false); } } diff --git a/src/main/java/seedu/address/logic/commands/LinkLoanCommand.java b/src/main/java/seedu/address/logic/commands/LinkLoanCommand.java new file mode 100644 index 00000000000..6f769635f6d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LinkLoanCommand.java @@ -0,0 +1,186 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_RETURN_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_VALUE; + +import java.math.BigDecimal; +import java.util.Date; +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.Loan; +import seedu.address.model.person.Person; + +/** + * Links a loan to a person in the address book. + */ +public class LinkLoanCommand extends Command { + + public static final String COMMAND_WORD = "linkloan"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Links a loan to the person identified " + + "by the index number used in the displayed person list. \n" + + "Parameters: " + + "INDEX " + + PREFIX_VALUE + "VALUE " + + PREFIX_START_DATE + "START_DATE " + + PREFIX_RETURN_DATE + "RETURN_DATE\n" + + "INDEX must be a positive integer.\n" + + "Example: " + COMMAND_WORD + " " + + "5 " + + PREFIX_VALUE + "500.00 " + + PREFIX_START_DATE + "2024-02-15 " + + PREFIX_RETURN_DATE + "2024-04-21\n" + + "This links a loan to the 5th person in view with a value of $500, " + + "a start date of 15 Feb 2024 " + + "and an end date of 21 April 2024."; + + public static final String MESSAGE_SUCCESS = "New loan linked to: %1$s\n" + + "%2$s"; + + private final LinkLoanDescriptor toLink; + + private final Index linkTarget; + + /** + * @param loanDescription of the loan to be linked + * @param index of the person in the filtered person list to link the loan to + */ + public LinkLoanCommand(LinkLoanDescriptor loanDescription, Index index) { + requireAllNonNull(loanDescription, index); + requireAllNonNull(loanDescription.getValue(), loanDescription.getStartDate(), + loanDescription.getReturnDate()); + toLink = loanDescription; + linkTarget = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + assert lastShownList != null; + + if (linkTarget.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person targetPerson = lastShownList.get(linkTarget.getZeroBased()); + + Loan linkedLoan = model.addLoan(toLink, targetPerson); + + return new CommandResult(String.format(MESSAGE_SUCCESS, targetPerson.getName(), + linkedLoan)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof LinkLoanCommand)) { + return false; + } + + LinkLoanCommand otherLinkLoanCommand = (LinkLoanCommand) other; + return toLink.equals(otherLinkLoanCommand.toLink) + && linkTarget.equals(otherLinkLoanCommand.linkTarget); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("linkTarget", linkTarget) + .add("loanDescription", toLink) + .toString(); + } + + /** + * Stores the details of the loan to be linked. + */ + public static class LinkLoanDescriptor { + private BigDecimal value; + private Date startDate; + private Date returnDate; + + /** + * Creates a instance of this LinkLoanDescriptor + * @param value The value of the loan to be linked + * @param startDate The start date of the loan + * @param returnDate The date which the loan must be returned by + */ + public LinkLoanDescriptor(BigDecimal value, Date startDate, Date returnDate) { + this.value = value; + this.startDate = startDate; + this.returnDate = returnDate; + } + + /** + * Copy constructor. + */ + public LinkLoanDescriptor(LinkLoanCommand.LinkLoanDescriptor toCopy) { + setValue(toCopy.value); + setStartDate(toCopy.startDate); + setReturnDate(toCopy.returnDate); + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + public BigDecimal getValue() { + return value; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getStartDate() { + return startDate; + } + + public void setReturnDate(Date returnDate) { + this.returnDate = returnDate; + } + + public Date getReturnDate() { + return returnDate; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof LinkLoanDescriptor)) { + return false; + } + + LinkLoanDescriptor otherLinkLoanDescriptor = (LinkLoanDescriptor) other; + return (value == otherLinkLoanDescriptor.value) + && Objects.equals(startDate, otherLinkLoanDescriptor.startDate) + && Objects.equals(returnDate, otherLinkLoanDescriptor.returnDate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("value", value) + .add("startDate", startDate) + .add("returnDate", returnDate) + .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..730001e85aa 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); + model.setToPersonTab(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/MarkLoanCommand.java b/src/main/java/seedu/address/logic/commands/MarkLoanCommand.java new file mode 100644 index 00000000000..f0f8d9712ef --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/MarkLoanCommand.java @@ -0,0 +1,77 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Loan; + + +/** + * Marks a loan as paid. + */ +public class MarkLoanCommand extends Command { + public static final String COMMAND_WORD = "markloan"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Marks the current person in view's loan(of loan number) as paid.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1\n"; + public static final String MESSAGE_SUCCESS = "Loan marked.\n" + + "Loan: %1$s"; + public static final String MESSAGE_FAILURE_LOAN = "No loan has been found " + + "for loan number: %1$d"; + private final Index loanIndex; + + /** + * Creates a MarkLoanCommand to mark the specified loan. + * @param loanIndex Index of the loan in the last shown loan list. + */ + public MarkLoanCommand(Index loanIndex) { + requireAllNonNull(loanIndex); + this.loanIndex = loanIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getSortedLoanList(); + assert lastShownList != null; + if (loanIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(String.format(MESSAGE_FAILURE_LOAN, loanIndex.getOneBased())); + } + // mark specified loan number + Loan loanToMark = lastShownList.get(loanIndex.getZeroBased()); + model.markLoan(loanToMark); + return new CommandResult(generateSuccessMessage(loanToMark), false, false, true); + } + + /** + * Generates a command execution success message after loan is marked. + */ + private String generateSuccessMessage(Loan markedLoan) { + return String.format(MESSAGE_SUCCESS, markedLoan); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + // instanceof handles nulls + if (!(other instanceof MarkLoanCommand)) { + return false; + } + MarkLoanCommand e = (MarkLoanCommand) other; + return loanIndex.equals(e.loanIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("loanIndex", loanIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/UnmarkLoanCommand.java b/src/main/java/seedu/address/logic/commands/UnmarkLoanCommand.java new file mode 100644 index 00000000000..d876ab94813 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UnmarkLoanCommand.java @@ -0,0 +1,76 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Loan; + +/** + * Reverts the status of a loan to unpaid. + */ +public class UnmarkLoanCommand extends Command { + public static final String COMMAND_WORD = "unmarkloan"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Marks the current person in view's loan(of loan number) as not paid.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1\n"; + public static final String MESSAGE_SUCCESS = "Loan unmarked.\n" + + "Loan: %1$s"; + public static final String MESSAGE_FAILURE_LOAN = "No loan has been found " + + "for loan number: %1$d"; + private final Index loanIndex; + + /** + * Creates a UnmarkLoanCommand to unmark the specified loan. + * @param loanIndex Index of the loan in the last shown loan list. + */ + public UnmarkLoanCommand(Index loanIndex) { + requireAllNonNull(loanIndex); + this.loanIndex = loanIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getSortedLoanList(); + assert lastShownList != null; + if (loanIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(String.format(MESSAGE_FAILURE_LOAN, loanIndex.getOneBased())); + } + // unmark specified loan number + Loan loanToUnmark = lastShownList.get(loanIndex.getZeroBased()); + model.unmarkLoan(loanToUnmark); + return new CommandResult(generateSuccessMessage(loanToUnmark), false, false, true); + } + + /** + * Generates a command execution success message after loan is unmarked. + */ + private String generateSuccessMessage(Loan markedLoan) { + return String.format(MESSAGE_SUCCESS, markedLoan); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + // instanceof handles nulls + if (!(other instanceof UnmarkLoanCommand)) { + return false; + } + UnmarkLoanCommand e = (UnmarkLoanCommand) other; + return loanIndex.equals(e.loanIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("loanIndex", loanIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ViewLoanCommand.java b/src/main/java/seedu/address/logic/commands/ViewLoanCommand.java new file mode 100644 index 00000000000..e90b9662a77 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewLoanCommand.java @@ -0,0 +1,77 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Represents a command to view the loans associated with a contact. + */ +public class ViewLoanCommand extends ViewLoanRelatedCommand { + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": View loans associated with the person identified by the index used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_SUCCESS = "Listed all loans associated with: %1$s"; + private final Index targetIndex; + + /** + * Creates a ViewLoanCommand to view the loans associated with the person at the specified {@code Index}. + * + * @param targetIndex The index of the person to view loans for. + * @param isShowAllLoans Whether to show all loans or only active loans. + */ + public ViewLoanCommand(Index targetIndex, boolean isShowAllLoans) { + super(isShowAllLoans); + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + assert lastShownList != null; + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToShowLoan = lastShownList.get(targetIndex.getZeroBased()); + model.updateFilteredPersonList(person -> person.equals(personToShowLoan)); + model.updateFilteredLoanList(loan -> loan.isAssignedTo(personToShowLoan), isShowAllLoans); + model.setDualPanel(); + model.setIsShowLoaneeInfo(false); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(personToShowLoan)), + false, false, true); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ViewLoanCommand)) { + return false; + } + + ViewLoanCommand otherViewLoanCommand = (ViewLoanCommand) other; + return targetIndex.equals(otherViewLoanCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ViewLoanRelatedCommand.java b/src/main/java/seedu/address/logic/commands/ViewLoanRelatedCommand.java new file mode 100644 index 00000000000..2caceb5a5d8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewLoanRelatedCommand.java @@ -0,0 +1,13 @@ +package seedu.address.logic.commands; + +/** + * Represents a command that is related to viewing loans. + */ +public abstract class ViewLoanRelatedCommand extends Command { + public static final String COMMAND_WORD = "viewloan"; + final boolean isShowAllLoans; + + protected ViewLoanRelatedCommand(boolean isShowAllLoans) { + this.isShowAllLoans = isShowAllLoans; + } +} diff --git a/src/main/java/seedu/address/logic/commands/ViewLoansCommand.java b/src/main/java/seedu/address/logic/commands/ViewLoansCommand.java new file mode 100644 index 00000000000..27f1cccd5f2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewLoansCommand.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_NO_PERSONS; + +import seedu.address.model.Model; + +/** + * Lists all active loans in the address book to the user. + */ +public class ViewLoansCommand extends ViewLoanRelatedCommand { + + public static final String MESSAGE_SUCCESS = "Listed all loans"; + + /** + * @param isShowAllLoans Whether to show all loans or only active loans. + */ + public ViewLoansCommand(boolean isShowAllLoans) { + super(isShowAllLoans); + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(PREDICATE_SHOW_NO_PERSONS); + model.updateFilteredLoanList(unused -> true, isShowAllLoans); + model.setIsLoansTab(true); + model.setIsShowLoaneeInfo(true); + return new CommandResult(MESSAGE_SUCCESS, false, false, true); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..f923c445461 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -9,14 +9,21 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AnalyticsCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteLoanCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditLoanCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.LinkLoanCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.MarkLoanCommand; +import seedu.address.logic.commands.UnmarkLoanCommand; +import seedu.address.logic.commands.ViewLoanRelatedCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -52,7 +59,6 @@ public Command parseCommand(String userInput) throws ParseException { logger.fine("Command word: " + commandWord + "; Arguments: " + arguments); switch (commandWord) { - case AddCommand.COMMAND_WORD: return new AddCommandParser().parse(arguments); @@ -68,15 +74,36 @@ public Command parseCommand(String userInput) throws ParseException { case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case LinkLoanCommand.COMMAND_WORD: + return new LinkLoanCommandParser().parse(arguments); + case ListCommand.COMMAND_WORD: return new ListCommand(); + case DeleteLoanCommand.COMMAND_WORD: + return new DeleteLoanCommandParser().parse(arguments); + case ExitCommand.COMMAND_WORD: return new ExitCommand(); case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case ViewLoanRelatedCommand.COMMAND_WORD: + return new ViewLoanCommandParser().parse(arguments); + + case MarkLoanCommand.COMMAND_WORD: + return new MarkLoanCommandParser().parse(arguments); + + case UnmarkLoanCommand.COMMAND_WORD: + return new UnmarkLoanCommandParser().parse(arguments); + + case AnalyticsCommand.COMMAND_WORD: + return new AnalyticsCommandParser().parse(arguments); + + case EditLoanCommand.COMMAND_WORD: + return new EditLoanCommandParser().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/AnalyticsCommandParser.java b/src/main/java/seedu/address/logic/parser/AnalyticsCommandParser.java new file mode 100644 index 00000000000..a9a638114a4 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AnalyticsCommandParser.java @@ -0,0 +1,30 @@ +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.AnalyticsCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AnalyticsCommand object + */ +public class AnalyticsCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AnalyticsCommand + * and returns an AnalyticsCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public AnalyticsCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new AnalyticsCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AnalyticsCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..809a218c43d 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -11,5 +11,9 @@ public class CliSyntax { public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); - + public static final Prefix PREFIX_VALUE = new Prefix("v/"); + public static final Prefix PREFIX_START_DATE = new Prefix("s/"); + public static final Prefix PREFIX_RETURN_DATE = new Prefix("r/"); + public static final Prefix PREFIX_LOAN_INDEX = new Prefix("l/"); + public static final String FLAG_SHOW_ALL_LOANS = "-a"; } diff --git a/src/main/java/seedu/address/logic/parser/DeleteLoanCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteLoanCommandParser.java new file mode 100644 index 00000000000..ab5c3ff68b5 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteLoanCommandParser.java @@ -0,0 +1,38 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteLoanCommand; +import seedu.address.logic.parser.exceptions.ParseException; + + +/** + * Parses input arguments and creates a new DeleteLoanCommand object. + */ +public class DeleteLoanCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the DeleteLoanCommand + * and returns a DeleteLoanCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteLoanCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteLoanCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteLoanCommand.MESSAGE_USAGE), pe); + } + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditLoanCommandParser.java b/src/main/java/seedu/address/logic/parser/EditLoanCommandParser.java new file mode 100644 index 00000000000..ea95affb621 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditLoanCommandParser.java @@ -0,0 +1,67 @@ +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_RETURN_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_VALUE; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditLoanCommand; +import seedu.address.logic.commands.EditLoanCommand.EditLoanDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditLoanCommand object + */ +public class EditLoanCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditLoanCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditLoanCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_VALUE, PREFIX_START_DATE, PREFIX_RETURN_DATE); + + if (arePrefixesEmpty(argMultimap, PREFIX_VALUE, PREFIX_START_DATE, PREFIX_RETURN_DATE) + || argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditLoanCommand.MESSAGE_USAGE)); + } + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditLoanCommand.MESSAGE_USAGE), pe); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_VALUE, PREFIX_START_DATE, PREFIX_RETURN_DATE); + + EditLoanDescriptor editLoanDescriptor = new EditLoanDescriptor(); + + if (argMultimap.getValue(PREFIX_VALUE).isPresent()) { + editLoanDescriptor.setValue(ParserUtil.parseValue(argMultimap.getValue(PREFIX_VALUE).get())); + } + if (argMultimap.getValue(PREFIX_START_DATE).isPresent()) { + editLoanDescriptor.setStartDate(ParserUtil.parseDate(argMultimap.getValue(PREFIX_START_DATE).get())); + } + if (argMultimap.getValue(PREFIX_RETURN_DATE).isPresent()) { + editLoanDescriptor.setReturnDate(ParserUtil.parseDate(argMultimap.getValue(PREFIX_RETURN_DATE).get())); + } + + return new EditLoanCommand(editLoanDescriptor, index); + } + + /** + * Returns true if all the prefixes contain empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesEmpty(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isEmpty()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/LinkLoanCommandParser.java b/src/main/java/seedu/address/logic/parser/LinkLoanCommandParser.java new file mode 100644 index 00000000000..6712a8c24ad --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/LinkLoanCommandParser.java @@ -0,0 +1,59 @@ +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_RETURN_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_START_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_VALUE; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.LinkLoanCommand; +import seedu.address.logic.commands.LinkLoanCommand.LinkLoanDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new LinkLoanCommand object + */ +public class LinkLoanCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an LinkLoanCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public LinkLoanCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_VALUE, PREFIX_START_DATE, PREFIX_RETURN_DATE); + + if (!arePrefixesPresent(argMultimap, PREFIX_VALUE, PREFIX_START_DATE, PREFIX_RETURN_DATE) + || argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, LinkLoanCommand.MESSAGE_USAGE)); + } + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, LinkLoanCommand.MESSAGE_USAGE), pe); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_VALUE, PREFIX_START_DATE, PREFIX_RETURN_DATE); + + LinkLoanDescriptor linkLoanDescriptor = ParserUtil.parseLoan(argMultimap.getValue(PREFIX_VALUE).get(), + argMultimap.getValue(PREFIX_START_DATE).get(), + argMultimap.getValue(PREFIX_RETURN_DATE).get()); + + return new LinkLoanCommand(linkLoanDescriptor, index); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/MarkLoanCommandParser.java b/src/main/java/seedu/address/logic/parser/MarkLoanCommandParser.java new file mode 100644 index 00000000000..c81a3784109 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/MarkLoanCommandParser.java @@ -0,0 +1,37 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.MarkLoanCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new MarkLoanCommand object. + */ +public class MarkLoanCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the MarkLoanCommand + * and returns a MarkLoanCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public MarkLoanCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new MarkLoanCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MarkLoanCommand.MESSAGE_USAGE), pe); + } + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..42f283da47b 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -1,16 +1,23 @@ package seedu.address.logic.parser; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.math.BigDecimal; import java.util.Collection; +import java.util.Date; import java.util.HashSet; import java.util.Set; import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.DateUtil; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.commands.LinkLoanCommand.LinkLoanDescriptor; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; import seedu.address.model.person.Email; +import seedu.address.model.person.Loan; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -121,4 +128,55 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses {@code String value}, {@code String startDate}, + * {@code String returnDate} into a {@code LinkLoanCommand.LinkLoanDescriptor}. + */ + public static LinkLoanDescriptor parseLoan(String value, String startDate, String returnDate) + throws ParseException { + requireAllNonNull(value, startDate, returnDate); + BigDecimal convertedValue = parseValue(value); + Date convertedStartDate = parseDate(startDate); + Date convertedReturnDate = parseDate(returnDate); + if (!Loan.isValidDates(convertedStartDate, convertedReturnDate)) { + throw new ParseException(Loan.DATE_CONSTRAINTS); + } + return new LinkLoanDescriptor(convertedValue, convertedStartDate, convertedReturnDate); + } + + /** + * Parses loan {@code String value} into a {@code BigDecimal}. + */ + public static BigDecimal parseValue(String value) throws ParseException { + requireNonNull(value); + String trimmedValue = value.trim(); + BigDecimal convertedValue; + try { + convertedValue = new BigDecimal(trimmedValue); + } catch (NumberFormatException n) { + // Ths is caught when the formatter is unable to parse the value correctly + throw new ParseException(Loan.VALUE_CONSTRAINTS); + } + if (!Loan.isValidValue(convertedValue)) { + throw new ParseException(Loan.VALUE_CONSTRAINTS); + } + return convertedValue; + } + + /** + * Parses {@code String date} into a {@code Date}. + */ + public static Date parseDate(String date) throws ParseException { + requireNonNull(date); + String trimmedDate = date.trim(); + Date convertedDate; + try { + convertedDate = DateUtil.parse(trimmedDate); + } catch (IllegalValueException i) { + // This is caught when the formatter is unable to parse the date correctly + throw new ParseException(Loan.DATE_CONSTRAINTS); + } + return convertedDate; + } } diff --git a/src/main/java/seedu/address/logic/parser/UnmarkLoanCommandParser.java b/src/main/java/seedu/address/logic/parser/UnmarkLoanCommandParser.java new file mode 100644 index 00000000000..60a66f283b0 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/UnmarkLoanCommandParser.java @@ -0,0 +1,38 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.UnmarkLoanCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new MarkLoanCommand object. + */ +public class UnmarkLoanCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the MarkLoanCommand + * and returns a MarkLoanCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public UnmarkLoanCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new UnmarkLoanCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnmarkLoanCommand.MESSAGE_USAGE), pe); + } + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} + diff --git a/src/main/java/seedu/address/logic/parser/ViewLoanCommandParser.java b/src/main/java/seedu/address/logic/parser/ViewLoanCommandParser.java new file mode 100644 index 00000000000..ff2ff4eb48a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ViewLoanCommandParser.java @@ -0,0 +1,46 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.FLAG_SHOW_ALL_LOANS; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ViewLoanCommand; +import seedu.address.logic.commands.ViewLoanRelatedCommand; +import seedu.address.logic.commands.ViewLoansCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ViewLoanCommand object + */ +public class ViewLoanCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ViewLoanCommand + * and returns a ViewLoanRelatedCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ViewLoanRelatedCommand parse(String args) throws ParseException { + boolean isShowAllLoans = false; + String modifiedArgs = args; + + // Check if the user wants to view all loans, active or not + if (modifiedArgs.contains(FLAG_SHOW_ALL_LOANS)) { + isShowAllLoans = true; + modifiedArgs = modifiedArgs.replace(FLAG_SHOW_ALL_LOANS, "").trim(); + } + + // Check if the user wants to view all loans, i.e. no index is provided + if (modifiedArgs.isEmpty()) { + return new ViewLoansCommand(isShowAllLoans); + } + + // Check if the user wants to view a specific person's loans + try { + Index index = ParserUtil.parseIndex(modifiedArgs); + return new ViewLoanCommand(index, isShowAllLoans); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewLoanCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..0aba37674ab 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -3,10 +3,14 @@ import static java.util.Objects.requireNonNull; import java.util.List; +import java.util.Objects; import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.LinkLoanCommand; +import seedu.address.model.person.Loan; import seedu.address.model.person.Person; +import seedu.address.model.person.UniqueLoanList; import seedu.address.model.person.UniquePersonList; /** @@ -16,6 +20,7 @@ public class AddressBook implements ReadOnlyAddressBook { private final UniquePersonList persons; + private final UniqueLoanList loans; /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication @@ -24,11 +29,14 @@ public class AddressBook implements ReadOnlyAddressBook { * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication * among constructors. */ + { persons = new UniquePersonList(); + loans = new UniqueLoanList(); } - public AddressBook() {} + public AddressBook() { + } /** * Creates an AddressBook using the Persons in the {@code toBeCopied} @@ -48,6 +56,14 @@ public void setPersons(List persons) { this.persons.setPersons(persons); } + /** + * Replaces the contents of the loan list with {@code loans}. + * {@code loans} must not contain duplicate loans. + */ + public void setLoans(List loans) { + this.loans.setLoans(loans); + } + /** * Resets the existing data of this {@code AddressBook} with {@code newData}. */ @@ -55,6 +71,7 @@ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); setPersons(newData.getPersonList()); + setLoans(newData.getLoanList()); } //// person-level operations @@ -67,6 +84,14 @@ public boolean hasPerson(Person person) { return persons.contains(person); } + /** + * Returns true if a person with the same identity as {@code person} exists in the address book. + */ + public boolean hasExactPerson(Person person) { + requireNonNull(person); + return persons.containsExact(person); + } + /** * Adds a person to the address book. * The person must not already exist in the address book. @@ -84,6 +109,7 @@ public void setPerson(Person target, Person editedPerson) { requireNonNull(editedPerson); persons.setPerson(target, editedPerson); + loans.modifyLoanAssignee(target, editedPerson); } /** @@ -92,6 +118,51 @@ public void setPerson(Person target, Person editedPerson) { */ public void removePerson(Person key) { persons.remove(key); + loans.removeLoansAttachedTo(key); + } + + //// loan-level operations + + /** + * Returns true if a loan with the same identity as {@code loan} exists in the address book. + */ + public boolean hasLoan(Loan loan) { + requireNonNull(loan); + return loans.contains(loan); + } + + /** + * Adds a loan to the address book. + * The loan must not already exist in the address book. + */ + public void addLoan(Loan l) { + loans.addLoan(l); + } + + public Loan addLoan(LinkLoanCommand.LinkLoanDescriptor loanDescription, Person assignee) { + return loans.addLoan(loanDescription, assignee); + } + /** + * Marks a loan in the address book. + * The loan must exist in the address book. + */ + public void markLoan(Loan loanToMark) { + loans.markLoan(loanToMark); + } + /** + * Unmarks a loan in the address book. + * The loan must exist in the address book. + */ + public void unmarkLoan(Loan loanToUnmark) { + loans.unmarkLoan(loanToUnmark); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the address book. + */ + public void removeLoan(Loan key) { + loans.removeLoan(key); } //// util methods @@ -108,6 +179,15 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList getLoanList() { + return loans.asUnmodifiableObservableList(); + } + + public UniqueLoanList getUniqueLoanList() { + return loans; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -120,11 +200,11 @@ public boolean equals(Object other) { } AddressBook otherAddressBook = (AddressBook) other; - return persons.equals(otherAddressBook.persons); + return persons.equals(otherAddressBook.persons) && loans.equals(otherAddressBook.loans); } @Override public int hashCode() { - return persons.hashCode(); + return Objects.hash(persons, loans); } } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..41252360041 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -3,17 +3,45 @@ import java.nio.file.Path; import java.util.function.Predicate; +import javafx.beans.property.ObjectProperty; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.commands.LinkLoanCommand; +import seedu.address.model.analytics.DashboardData; +import seedu.address.model.person.Analytics; +import seedu.address.model.person.Loan; import seedu.address.model.person.Person; +import seedu.address.model.tabindicator.TabIndicator; /** * The API of the Model component. */ public interface Model { - /** {@code Predicate} that always evaluate to true */ + /** + * {@code Predicate} that always evaluates to true + */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** + * {@code Predicate} that always evaluates to false + */ + Predicate PREDICATE_SHOW_NO_PERSONS = unused -> false; + + /** + * {@code Predicate} that always evaluate to true + */ + Predicate PREDICATE_SHOW_ALL_LOANS = unused -> true; + + /** + * {@code Predicate} that evaluates to true if the loan is active + */ + Predicate PREDICATE_SHOW_ALL_ACTIVE_LOANS = loan -> loan.isActive(); + + /** + * {@code Predicate} that always evaluates to false + */ + Predicate PREDICATE_SHOW_NO_LOANS = unused -> false; + /** * Replaces user prefs data with the data in {@code userPrefs}. */ @@ -49,7 +77,9 @@ public interface Model { */ void setAddressBook(ReadOnlyAddressBook addressBook); - /** Returns the AddressBook */ + /** + * Returns the AddressBook + */ ReadOnlyAddressBook getAddressBook(); /** @@ -76,12 +106,85 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); - /** Returns an unmodifiable view of the filtered person list */ + /** + * Returns true if a loan with the same identity as {@code loan} exists in the address book. + */ + boolean hasLoan(Loan loan); + + /** + * Deletes the given loan. + * The loan must exist in the address book. + */ + void deleteLoan(Loan target); + + /** + * Adds the given loan. + * {@code loan} must not already exist in the address book. + */ + void addLoan(Loan loan); + + Loan addLoan(LinkLoanCommand.LinkLoanDescriptor loanDescription, Person assignee); + + /** + * Returns an unmodifiable view of the filtered person list + */ ObservableList getFilteredPersonList(); /** * 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); + + /** + * Returns an unmodifiable view of the sorted loan list + */ + ObservableList getSortedLoanList(); + + /** + * Updates the filter of the filtered loan list to filter by the given {@code predicate}. + * + * @param predicate + */ + void updateFilteredLoanList(Predicate predicate); + + /** + * Updates the filter of the filtered loan list to filter by the given {@code predicate}. + * Also updates the preference of whether to show all loans or only active loans. + * + * @param predicate + * @param isShowAllLoans + */ + void updateFilteredLoanList(Predicate predicate, boolean isShowAllLoans); + + /** + * Sets the tab to the loans tab. + * + * @param isLoansTab + */ + void setIsLoansTab(Boolean isLoansTab); + + /** + * Sets the tab to the analytics tab. + * + * @param isAnalyticsTab + */ + void setIsAnalyticsTab(Boolean isAnalyticsTab); + + void setToPersonTab(); + + void markLoan(Loan loanToMark); + + void generateDashboardData(Analytics analytics); + + void unmarkLoan(Loan loanToUnmark); + + ObjectProperty getDashboardData(); + + void setDualPanel(); + + void setIsShowLoaneeInfo(Boolean isShowLoaneeInfo); + + ObjectProperty getTabIndicator(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..dbb45ffc5e2 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -3,15 +3,25 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.math.BigDecimal; import java.nio.file.Path; +import java.util.Date; import java.util.function.Predicate; import java.util.logging.Logger; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; 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.logic.commands.LinkLoanCommand; +import seedu.address.model.analytics.DashboardData; +import seedu.address.model.person.Analytics; +import seedu.address.model.person.Loan; import seedu.address.model.person.Person; +import seedu.address.model.tabindicator.TabIndicator; /** * Represents the in-memory model of the address book data. @@ -22,6 +32,12 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final FilteredList filteredLoans; + private final SortedList sortedLoans; + private final ObjectProperty tabIndicator = new SimpleObjectProperty<>(new TabIndicator(false, + false, true, false, false)); + + private final ObjectProperty dashboardData = new SimpleObjectProperty<>(); /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -34,6 +50,9 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredLoans = new FilteredList<>(this.addressBook.getLoanList()); + sortedLoans = new SortedList<>(filteredLoans, Loan::compareTo); + dashboardData.setValue(null); } public ModelManager() { @@ -111,7 +130,38 @@ public void setPerson(Person target, Person editedPerson) { addressBook.setPerson(target, editedPerson); } - //=========== Filtered Person List Accessors ============================================================= + @Override + public boolean hasLoan(Loan loan) { + requireNonNull(loan); + return addressBook.hasLoan(loan); + } + + @Override + public void deleteLoan(Loan target) { + addressBook.removeLoan(target); + } + + @Override + public void addLoan(Loan loan) { + addressBook.addLoan(loan); + } + + @Override + public Loan addLoan(LinkLoanCommand.LinkLoanDescriptor loanDescription, Person assignee) { + return addressBook.addLoan(loanDescription, assignee); + } + + @Override + public void markLoan(Loan loanToMark) { + addressBook.markLoan(loanToMark); + } + + @Override + public void unmarkLoan(Loan loanToUnmark) { + addressBook.unmarkLoan(loanToUnmark); + } + + //=========== Filtered Lists Accessors ============================================================= /** * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of @@ -128,6 +178,27 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } + @Override + public ObservableList getSortedLoanList() { + return sortedLoans; + } + + @Override + public void updateFilteredLoanList(Predicate predicate) { + requireNonNull(predicate); + Predicate secondPredicate = + this.tabIndicator.getValue().getIsShowAllLoans() ? PREDICATE_SHOW_ALL_LOANS + : PREDICATE_SHOW_ALL_ACTIVE_LOANS; + filteredLoans.setPredicate(predicate.and(secondPredicate)); + } + + @Override + public void updateFilteredLoanList(Predicate predicate, boolean isShowAllLoans) { + requireNonNull(predicate); + this.tabIndicator.setValue(this.tabIndicator.getValue().setIsShowAllLoans(isShowAllLoans)); + updateFilteredLoanList(predicate); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -142,7 +213,55 @@ 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) + && sortedLoans.equals(otherModelManager.sortedLoans); + } + + @Override + public void setIsLoansTab(Boolean isLoansTab) { + this.tabIndicator.setValue(this.tabIndicator.getValue().setIsLoansTab(isLoansTab)); + } + + + @Override + public void setToPersonTab() { + this.updateFilteredLoanList(PREDICATE_SHOW_NO_LOANS); + this.tabIndicator.setValue(this.tabIndicator.getValue().setIsPersonTab(true)); } + @Override + public void setIsAnalyticsTab(Boolean isAnalyticsTab) { + if (isAnalyticsTab) { + this.updateFilteredPersonList(PREDICATE_SHOW_NO_PERSONS); + this.updateFilteredLoanList(PREDICATE_SHOW_NO_LOANS); + } + this.tabIndicator.setValue(this.tabIndicator.getValue().setIsAnalyticsTab(isAnalyticsTab)); + } + + @Override + public ObjectProperty getDashboardData() { + return dashboardData; + } + + @Override + public void generateDashboardData(Analytics analytics) { + BigDecimal impactBenchmark = this.addressBook.getUniqueLoanList().getMaxLoanValue(); + Date urgencyBenchmark = this.addressBook.getUniqueLoanList().getEarliestReturnDate(); + dashboardData.setValue(new DashboardData(analytics, impactBenchmark, urgencyBenchmark)); + } + + @Override + public void setDualPanel() { + this.tabIndicator.setValue(this.tabIndicator.getValue().setDualPanelView()); + } + + @Override + public void setIsShowLoaneeInfo(Boolean isShowLoaneeInfo) { + this.tabIndicator.setValue(this.tabIndicator.getValue().setIsShowLoaneeInfo(isShowLoaneeInfo)); + } + + @Override + public ObjectProperty getTabIndicator() { + return this.tabIndicator; + } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..eef0499e913 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,6 +1,7 @@ package seedu.address.model; import javafx.collections.ObservableList; +import seedu.address.model.person.Loan; import seedu.address.model.person.Person; /** @@ -14,4 +15,5 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + ObservableList getLoanList(); } diff --git a/src/main/java/seedu/address/model/analytics/DashboardData.java b/src/main/java/seedu/address/model/analytics/DashboardData.java new file mode 100644 index 00000000000..ecf4fab7256 --- /dev/null +++ b/src/main/java/seedu/address/model/analytics/DashboardData.java @@ -0,0 +1,87 @@ +package seedu.address.model.analytics; + +import static java.util.Objects.requireNonNull; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; + +import seedu.address.model.person.Analytics; + + +/** + * Represents the analytics data of the dashboard with 3 values + * - Analytics object to be displayed + * - Max loan value + * - Earliest return date + */ +public class DashboardData { + private final Analytics analytics; + private final BigDecimal maxLoanValue; + private final Date earliestReturnDate; + + /** + * Creates a DashboardData object with the given analytics, max loan value and earliest return date + * + * @param analytics analytics object to be displayed + * @param maxLoanValue maximum loan value of all loans + * @param earliestReturnDate earliest return date of all loans (not returned and not overdue) + */ + public DashboardData(Analytics analytics, BigDecimal maxLoanValue, Date earliestReturnDate) { + requireNonNull(analytics); + this.analytics = analytics; + this.maxLoanValue = maxLoanValue; + this.earliestReturnDate = earliestReturnDate; + } + + public Analytics getAnalytics() { + return analytics; + } + + public BigDecimal getMaxLoanValue() { + return maxLoanValue; + } + + /** + * Calculates the impact index of the dashboard data + * Impact index is calculated as the ratio of the average loan value to the maximum loan value + * to 2 decimal places. + * + * @return impact index between 0 and 1 + */ + public BigDecimal getImpactIndex() { + return analytics.getAverageLoanValue().divide(maxLoanValue, 2, RoundingMode.HALF_UP); + } + + /** + * Calculates the urgency index of the dashboard data + * Urgency index is calculated as the ratio of the number of days between the earliest return date and the current + * to the number of days between the earliest return date and the benchmark date + * + * @return urgency index between 0 and 1 + */ + public Float getUrgencyIndex() { + // both variables can be null if the lists are empty + if (analytics.getEarliestReturnDate() == null || earliestReturnDate == null) { + return null; + } + + LocalDate target = analytics.getEarliestReturnDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate benchmark = this.earliestReturnDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate now = LocalDate.now(); + long dayDiffBenchmark = benchmark.toEpochDay() - now.toEpochDay(); + long dayDiffTarget = target.toEpochDay() - now.toEpochDay(); + if (dayDiffTarget == 0) { + return 1.0f; + } + return (float) dayDiffBenchmark / dayDiffTarget; + } + + @Override + public String toString() { + return "Analytics: " + analytics + ", Max Loan Value: " + maxLoanValue + ", Earliest Return Date: " + + earliestReturnDate; + } +} diff --git a/src/main/java/seedu/address/model/person/Analytics.java b/src/main/java/seedu/address/model/person/Analytics.java new file mode 100644 index 00000000000..9641973aa78 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Analytics.java @@ -0,0 +1,199 @@ +package seedu.address.model.person; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Date; + +import javafx.collections.ObservableList; + +/** + * Represents the analytics of a LoanRecords object. + */ +public class Analytics { + + private int numLoans; // total number of loans + private int numOverdueLoans; // total number of overdue loans + private int numActiveLoans; // total number of active loans + + private float propOverdueLoans; // proportion of loans that are overdue over active loans + private float propActiveLoans; // proportion of loans that are active over total loans + + private BigDecimal totalValueLoaned; // total value of all loans + private BigDecimal totalValueOverdue; // total value of all overdue loans + private BigDecimal totalValueActive; // total value of all active loans + + private BigDecimal averageLoanValue; // average loan value of all loans + private BigDecimal averageOverdueValue; // average loan value of all overdue loans + private BigDecimal averageActiveValue; // average loan value of all active loans + + private Date earliestLoanDate; // earliest loan date of all loans + private Date earliestReturnDate; // earliest return date of active loans (not overdue) + private Date latestLoanDate; // latest loan date of all loans + private Date latestReturnDate; // latest return date of active loans + + private Analytics() { + this.numLoans = 0; + this.numOverdueLoans = 0; + this.numActiveLoans = 0; + + this.propOverdueLoans = 0; + this.propActiveLoans = 0; + + this.totalValueLoaned = BigDecimal.ZERO; + this.totalValueOverdue = BigDecimal.ZERO; + this.totalValueActive = BigDecimal.ZERO; + + this.averageLoanValue = BigDecimal.ZERO; + this.averageOverdueValue = BigDecimal.ZERO; + this.averageActiveValue = BigDecimal.ZERO; + + this.earliestLoanDate = null; + this.earliestReturnDate = null; + this.latestLoanDate = null; + this.latestReturnDate = null; + } + + /** + * Updates the fields that count the number of various loans. + * + * @param loan The loan to update the fields with. + */ + private void updateNumFields(Loan loan) { + this.numLoans++; + if (loan.isOverdue()) { + this.numOverdueLoans++; + } + if (loan.isActive()) { + this.numActiveLoans++; + } + } + + /** + * Updates the fields that calculate the proportion of various loans. + * This method should be called after the fields that count the number of various loans have been updated. + */ + private void updatePropFields() { + if (this.numActiveLoans > 0 && this.numLoans > 0) { + this.propActiveLoans = (float) this.numActiveLoans / this.numLoans; + this.propOverdueLoans = (float) this.numOverdueLoans / this.numActiveLoans; + } + } + + /** + * Updates the fields that calculate the total value of various loans. + * + * @param loan The loan to update the fields with. + */ + private void updateValueFields(Loan loan) { + totalValueLoaned = totalValueLoaned.add(loan.getValue()); + if (loan.isOverdue()) { + totalValueOverdue = totalValueOverdue.add(loan.getValue()); + } + if (loan.isActive()) { + totalValueActive = totalValueActive.add(loan.getValue()); + } + } + + /** + * Updates the fields that calculate the average value of various loans. + * This method should be called after the fields that calculate the total value of various loans have been updated. + */ + private void updateAverageFields() { + if (numActiveLoans > 0) { + averageActiveValue = totalValueActive.divide(BigDecimal.valueOf(numActiveLoans), + 2, RoundingMode.HALF_UP); + } + if (numOverdueLoans > 0) { + averageOverdueValue = totalValueOverdue.divide(BigDecimal.valueOf(numOverdueLoans), + 2, RoundingMode.HALF_UP); + } + if (numLoans > 0) { + averageLoanValue = totalValueLoaned.divide(BigDecimal.valueOf(this.numLoans), + 2, RoundingMode.HALF_UP); + } + } + + /** + * Updates the fields that calculate the earliest and latest dates of various loans. + * + * @param loan The loan to update the fields with. + */ + private void updateDateFields(Loan loan) { + if (this.earliestLoanDate == null || loan.getStartDate().before(this.earliestLoanDate)) { + this.earliestLoanDate = loan.getStartDate(); + } + if (this.latestLoanDate == null || loan.getStartDate().after(this.latestLoanDate)) { + this.latestLoanDate = loan.getStartDate(); + } + if (!loan.isReturned() && !loan.isOverdue()) { + if (this.earliestReturnDate == null || loan.getReturnDate().before(this.earliestReturnDate)) { + this.earliestReturnDate = loan.getReturnDate(); + } + if (this.latestReturnDate == null || loan.getReturnDate().after(this.latestReturnDate)) { + this.latestReturnDate = loan.getReturnDate(); + } + } + + } + + /** + * Returns an Analytics object that represents the analytics of a LoanRecords object. + * + * @param loanList The list of loans to calculate the analytics from. + * @return The Analytics object that represents the analytics of the LoanRecords object. + */ + public static Analytics getAnalytics(ObservableList loanList) { + UniqueLoanList uniqueLoanList = new UniqueLoanList(); + uniqueLoanList.setLoans(loanList); + Analytics analytics = new Analytics(); + for (int i = 0; i < uniqueLoanList.size(); i++) { + Loan loan = uniqueLoanList.getLoan(i); + analytics.updateNumFields(loan); + analytics.updateValueFields(loan); + analytics.updateDateFields(loan); + } + analytics.updatePropFields(); + analytics.updateAverageFields(); + return analytics; + } + + public int getNumActiveLoans() { + return numActiveLoans; + } + + public float getPropOverdueLoans() { + return propOverdueLoans; + } + + public float getPropActiveLoans() { + return propActiveLoans; + } + + public BigDecimal getAverageLoanValue() { + return averageLoanValue; + } + + public Date getEarliestReturnDate() { + return earliestReturnDate; + } + + @Override + public String toString() { + return "Number of loans: " + numLoans + "\n" + + "Number of overdue loans: " + numOverdueLoans + "\n" + + "Number of active loans: " + numActiveLoans + "\n" + + "Proportion of overdue loans: " + propOverdueLoans + "\n" + + "Proportion of active loans: " + propActiveLoans + "\n" + + "Total value loaned: " + totalValueLoaned + "\n" + + "Total value of overdue loans: " + totalValueOverdue + "\n" + + "Total value of active loans: " + totalValueActive + "\n" + + "Average loan value: " + averageLoanValue + "\n" + + "Average value of overdue loans: " + averageOverdueValue + "\n" + + "Average value of active loans: " + averageActiveValue + "\n" + + "Earliest loan date: " + earliestLoanDate + "\n" + + "Earliest return date: " + earliestReturnDate + "\n" + + "Latest loan date: " + latestLoanDate + "\n" + + "Latest return date: " + latestReturnDate + "\n"; + } + +} diff --git a/src/main/java/seedu/address/model/person/Loan.java b/src/main/java/seedu/address/model/person/Loan.java new file mode 100644 index 00000000000..c8ee318f6c3 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Loan.java @@ -0,0 +1,178 @@ +package seedu.address.model.person; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.math.BigDecimal; +import java.util.Date; + +import seedu.address.commons.util.DateUtil; + +/** + * Represents a Loan in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Loan implements Comparable { + + public static final String DATE_CONSTRAINTS = "Dates should be of the form " + DateUtil.DATE_FORMAT + + " and the loan start date must be before the return date."; + + public static final String VALUE_CONSTRAINTS = "Loan values must be a positive number."; + + private final int id; + private final BigDecimal value; + private final Date startDate; + private final Date returnDate; + private boolean isReturned; + private Person assignee; + + /** + * Constructs a {@code Loan} with a given id. + * + * @param id A valid id. + * @param value A valid value. + * @param startDate A valid start date. + * @param returnDate A valid return date. + * @param assignee A valid assignee. + */ + public Loan(int id, BigDecimal value, Date startDate, Date returnDate, Person assignee) { + requireAllNonNull(id, value, startDate, returnDate, assignee); + assert isValidValue(value); + assert id >= 0; + this.id = id; + this.value = value; + this.startDate = startDate; + this.returnDate = returnDate; + this.isReturned = false; + this.assignee = assignee; + } + + /** + * Constructs a {@code Loan} with a given id and return status. + * + * @param id A valid id. + * @param value A valid value. + * @param startDate A valid start date. + * @param returnDate A valid return date. + * @param isReturned A valid return status. + * @param assignee A valid assignee. + */ + public Loan(int id, BigDecimal value, Date startDate, Date returnDate, boolean isReturned, Person assignee) { + requireAllNonNull(id, value, startDate, returnDate, isReturned, assignee); + assert isValidValue(value); + assert id >= 0; + this.id = id; + this.value = value; + this.startDate = startDate; + this.returnDate = returnDate; + this.isReturned = isReturned; + this.assignee = assignee; + } + + /** + * Returns true if a given BigDecimal is a valid value. + */ + public static boolean isValidValue(BigDecimal value) { + return value.compareTo(BigDecimal.ZERO) > 0; + } + + /** + * Returns true if a given start date and return date are valid. + */ + public static boolean isValidDates(Date startDate, Date returnDate) { + return startDate.before(returnDate); + } + + public int getId() { + return id; + } + + public BigDecimal getValue() { + return value; + } + + public Date getStartDate() { + return startDate; + } + + public Date getReturnDate() { + return returnDate; + } + + public boolean isReturned() { + return isReturned; + } + + public boolean isActive() { + return !isReturned; + } + + public Person getAssignee() { + return assignee; + } + + public boolean isAssignedTo(Person person) { + return assignee.equals(person); + } + + public int compareTo(Loan other) { + return this.returnDate.compareTo(other.returnDate); + } + + /** + * Returns true if the loan is overdue. + */ + public boolean isOverdue() { + // shift return date to the next day + Date returnDateNextDay = DateUtil.addDay(returnDate, 1); + return !isReturned && new Date().after(returnDateNextDay); + } + + /** + * Marks the loan as returned. + */ + public void markAsReturned() { + isReturned = true; + } + + /** + * Marks the loan as not returned. + */ + public void unmarkAsReturned() { + isReturned = false; + } + + @Override + public String toString() { + if (isReturned) { + return String.format("$%.2f, %s, %s (Returned)", value, DateUtil.format(startDate), + DateUtil.format(returnDate)); + } else { + return String.format("$%.2f, %s, %s", value, DateUtil.format(startDate), + DateUtil.format(returnDate)); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Loan)) { + return false; + } + + Loan otherLoan = (Loan) other; + + return id == otherLoan.id; + } + + @Override + public int hashCode() { + return id; + } + + public void setAssignee(Person editedPerson) { + assignee = editedPerson; + } +} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..ff825db3c9e 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -28,7 +28,8 @@ public class Person { /** * 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, + Set tags) { requireAllNonNull(name, phone, email, address, tags); this.name = name; this.phone = phone; @@ -61,6 +62,7 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. diff --git a/src/main/java/seedu/address/model/person/UniqueLoanList.java b/src/main/java/seedu/address/model/person/UniqueLoanList.java new file mode 100644 index 00000000000..5cdd6f5c9a6 --- /dev/null +++ b/src/main/java/seedu/address/model/person/UniqueLoanList.java @@ -0,0 +1,363 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.DateUtil; +import seedu.address.logic.commands.LinkLoanCommand.LinkLoanDescriptor; +import seedu.address.model.person.exceptions.DuplicateLoanException; +import seedu.address.model.person.exceptions.LoanNotFoundException; + +/** + * Represents a list of loans in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class UniqueLoanList implements Iterable { + + private static final String DATE_MESSAGE_CONSTRAINTS = "Dates must be in the format dd-MM-yyyy."; + private static int nextLoanId = 1; + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent loan as the given argument. + */ + public boolean contains(Loan toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::equals); + } + + /** + * Adds a loan to the list of loans. + * + * @param loan A valid loan. + */ + public void addLoan(Loan loan) { + requireNonNull(loan); + internalList.add(loan); + updateNextLoanId(); + } + + /** + * Adds a loan to the list of loans. + * + * @param value A valid value. + * @param startDate A valid start date. + * @param returnDate A valid return date. + */ + public Loan addLoan(BigDecimal value, Date startDate, Date returnDate, Person assignee) { + Loan loan = new Loan(nextLoanId, value, startDate, returnDate, assignee); + addLoan(loan); + return loan; + } + + /** + * Adds a loan to the list of loans. + * + * @param loanDescription A valid LinkLoanDescriptor, which contains details about the loan to be added. + */ + public Loan addLoan(LinkLoanDescriptor loanDescription, Person assignee) { + BigDecimal value = loanDescription.getValue(); + Date startDate = loanDescription.getStartDate(); + Date returnDate = loanDescription.getReturnDate(); + return addLoan(value, startDate, returnDate, assignee); + } + + /** + * Adds a loan to the list of loans. + * + * @param value A valid value. + * @param startDate A valid start date. + * @param returnDate A valid return date. + * @throws IllegalValueException If the date string is not in the correct format. + */ + public void addLoan(BigDecimal value, String startDate, String returnDate, Person assignee) + throws IllegalValueException { + try { + Date start = DateUtil.parse(startDate); + Date end = DateUtil.parse(returnDate); + addLoan(value, start, end, assignee); + } catch (IllegalValueException e) { + throw new IllegalValueException(UniqueLoanList.DATE_MESSAGE_CONSTRAINTS); + } + } + + /** + * Removes a loan from the list of loans. + * + * @param toRemove A valid loan. + */ + public void removeLoan(Loan toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new LoanNotFoundException(); + } + } + + public void setPersons(UniqueLoanList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + public void setLoans(List replacement) { + requireNonNull(replacement); + if (!loansAreUnique(replacement)) { + throw new DuplicateLoanException(); + } + internalList.setAll(replacement); + for (Loan loan : replacement) { + nextLoanId = Math.max(nextLoanId, loan.getId() + 1); + } + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + /** + * @param idx A valid index. + * @return The loan at the specified index. + */ + public Loan getLoan(int idx) { + return internalList.get(idx); + } + + public Loan getLoanById(int id) { + for (Loan loan : internalList) { + if (loan.getId() == id) { + return loan; + } + } + return null; + } + + /** + * Marks a loan as returned. + * + * @param idx A valid index. + */ + public void markLoanAsReturned(int idx) { + internalList.get(idx).markAsReturned(); + } + + /** + * Marks a loan as returned. + * + * @param id A valid id. + */ + public void markLoanAsReturnedById(int id) { + Loan loan = getLoanById(id); + if (loan != null) { + loan.markAsReturned(); + } + } + + /** + * Marks a loan as not returned. + * + * @param loanToMark A valid loan. + */ + public void markLoan(Loan loanToMark) { + int index = internalList.indexOf(loanToMark); + + if (index == -1) { + throw new LoanNotFoundException(); + } + + loanToMark.markAsReturned(); + internalList.set(index, loanToMark); + } + + /** + * Marks a loan of the specified index as returned. + */ + public void markLoan(int idx) { + internalList.get(idx).markAsReturned(); + } + + /** + * @return A list of loans. + */ + public List getLoanList() { + return new ArrayList<>(internalList); + } + + /** + * @return The id of the next loan. + */ + public int getNextLoanId() { + return nextLoanId; + } + + /** + * Updates the id of the next loan. + */ + public void updateNextLoanId() { + nextLoanId++; + } + + /** + * Unmarks a loan. + * + * @param loanToUnmark A valid loan. + */ + public void unmarkLoan(Loan loanToUnmark) { + int index = internalList.indexOf(loanToUnmark); + + if (index == -1) { + throw new LoanNotFoundException(); + } + + loanToUnmark.unmarkAsReturned(); + internalList.set(index, loanToUnmark); + } + + /** + * Marks a loan of the specified index as not returned. + */ + public void unmarkLoan(int idx) { + internalList.get(idx).unmarkAsReturned(); + } + + /** + * @return The number of loans in the list. + */ + public int size() { + return internalList.size(); + } + + @Override + public String toString() { + String output = "Loans:\n"; + int idx = 1; + for (Loan loan : internalList) { + output += idx + ". " + loan.toString() + "\n"; + idx++; + } + return output; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof UniqueLoanList)) { + return false; + } + + UniqueLoanList otherUniqueLoanList = (UniqueLoanList) other; + // create hashset of ids of loans in this and other + HashSet thisLoanIds = new HashSet<>(); + HashSet otherLoanIds = new HashSet<>(); + for (Loan loan : internalList) { + thisLoanIds.add(loan.getId()); + } + for (Loan loan : otherUniqueLoanList.internalList) { + otherLoanIds.add(loan.getId()); + } + return thisLoanIds.equals(otherLoanIds); + } + + @Override + public int hashCode() { + return Objects.hash(internalList, nextLoanId); + } + + /** + * Returns true if {@code persons} contains only unique persons. + */ + private boolean loansAreUnique(List loans) { + for (int i = 0; i < loans.size() - 1; i++) { + for (int j = i + 1; j < loans.size(); j++) { + if (loans.get(i).equals(loans.get(j))) { + return false; + } + } + } + return true; + } + + /** + * Removes all loans attached to a person. + * + * @param key A valid person. + */ + public void removeLoansAttachedTo(Person key) { + internalList.removeIf(loan -> loan.getAssignee().equals(key)); + } + + + /** + * Modifies the assignee of all loans attached to a person. + * + * @param target A valid person. + * @param editedPerson A valid person. + */ + public void modifyLoanAssignee(Person target, Person editedPerson) { + for (Loan loan : internalList) { + if (loan.getAssignee().equals(target)) { + loan.setAssignee(editedPerson); + } + } + + // Just to update the list for GUI + if (!internalList.isEmpty()) { + internalList.set(0, internalList.get(0)); + } + } + + /** + * Returns the maximum loan value of all loans. + * + * @return The maximum loan value of all loans. + */ + public BigDecimal getMaxLoanValue() { + BigDecimal maxLoanValue = BigDecimal.ZERO; + for (Loan loan : internalList) { + if (loan.getValue().compareTo(maxLoanValue) > 0) { + maxLoanValue = loan.getValue(); + } + } + return maxLoanValue; + } + + /** + * Returns the earliest return date of all loans. + * The loan must not be overdue and must not have been returned. + * + * @return The earliest return date of all loans. Returns null if there are no loans that meet the criteria. + */ + public Date getEarliestReturnDate() { + Date earliestReturnDate = null; + for (Loan loan : internalList) { + if ((earliestReturnDate == null || loan.getReturnDate().before(earliestReturnDate)) + && !loan.isOverdue() && !loan.isReturned()) { + earliestReturnDate = loan.getReturnDate(); + } + } + return earliestReturnDate; + } +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index cc0a68d79f9..bb53c16ed83 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -36,6 +36,14 @@ public boolean contains(Person toCheck) { return internalList.stream().anyMatch(toCheck::isSamePerson); } + /** + * Returns true if the list contains an exact person as the given argument. + */ + public boolean containsExact(Person person) { + requireNonNull(person); + return internalList.stream().anyMatch(person::equals); + } + /** * Adds a person to the list. * The person must not already exist in the list. diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicateLoanException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicateLoanException.java new file mode 100644 index 00000000000..6fa1f26c699 --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/DuplicateLoanException.java @@ -0,0 +1,8 @@ +package seedu.address.model.person.exceptions; + +/** + * Signals that the operation will result in duplicate Loans (Loans are considered duplicates if they have the same + * identity). + */ +public class DuplicateLoanException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/person/exceptions/LoanNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/LoanNotFoundException.java new file mode 100644 index 00000000000..84101e44844 --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/LoanNotFoundException.java @@ -0,0 +1,7 @@ +package seedu.address.model.person.exceptions; + +/** + * Signals that the operation is unable to find the specified loan. + */ +public class LoanNotFoundException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/tabindicator/TabIndicator.java b/src/main/java/seedu/address/model/tabindicator/TabIndicator.java new file mode 100644 index 00000000000..b19e6c3db65 --- /dev/null +++ b/src/main/java/seedu/address/model/tabindicator/TabIndicator.java @@ -0,0 +1,106 @@ +package seedu.address.model.tabindicator; + +/** + * Represents the tab indicator of the dashboard. + * Determines which tab is currently shown in the GUI. + */ +public class TabIndicator { + private final boolean isLoansTab; + private final boolean isAnalyticsTab; + private final boolean isPersonTab; + private final boolean isShowAllLoans; + private final boolean isShowLoaneeInfo; + + /** + * Default constructor for TabIndicator. + * + * @param loans Whether the loans tab is shown. + * @param analytics Whether the analytics tab is shown. + * @param person Whether the person tab is shown. + * @param showAllLoans Whether to show all loans or only active loans. + * @param showLoaneeInfo Whether to show loanee information. + */ + public TabIndicator(boolean loans, boolean analytics, boolean person, boolean showAllLoans, + boolean showLoaneeInfo) { + this.isLoansTab = loans; + this.isAnalyticsTab = analytics; + this.isPersonTab = person; + this.isShowAllLoans = showAllLoans; + this.isShowLoaneeInfo = showLoaneeInfo; + } + + public boolean getIsLoansTab() { + return isLoansTab; + } + + public TabIndicator setIsLoansTab(boolean newIsLoansTab) { + if (newIsLoansTab) { + return new TabIndicator(newIsLoansTab, false, false, this.isShowAllLoans, this.isShowLoaneeInfo); + } + return new TabIndicator(newIsLoansTab, isAnalyticsTab, isPersonTab, this.isShowAllLoans, this.isShowLoaneeInfo); + + } + + public boolean getIsAnalyticsTab() { + return isAnalyticsTab; + } + + public TabIndicator setIsAnalyticsTab(boolean newIsAnalyticsTab) { + if (newIsAnalyticsTab) { + return new TabIndicator(false, newIsAnalyticsTab, false, + this.isShowAllLoans, this.isShowLoaneeInfo); + } + return new TabIndicator(this.isLoansTab, newIsAnalyticsTab, this.isPersonTab, + this.isShowAllLoans, this.isShowLoaneeInfo); + } + + public boolean getIsPersonTab() { + return isPersonTab; + } + + public TabIndicator setIsPersonTab(boolean newIsPersonTab) { + if (newIsPersonTab) { + return new TabIndicator(false, false, newIsPersonTab, + this.isShowAllLoans, this.isShowLoaneeInfo); + } + return new TabIndicator(this.isLoansTab, this.isAnalyticsTab, newIsPersonTab, + this.isShowAllLoans, this.isShowLoaneeInfo); + } + + public boolean getIsShowAllLoans() { + return isShowAllLoans; + } + + public TabIndicator setIsShowAllLoans(boolean newIsShowAllLoans) { + return new TabIndicator(this.isLoansTab, this.isAnalyticsTab, this.isPersonTab, + newIsShowAllLoans, this.isShowLoaneeInfo); + } + + public boolean getIsShowLoaneeInfo() { + return isShowLoaneeInfo; + } + + public TabIndicator setIsShowLoaneeInfo(boolean newIsShowLoaneeInfo) { + return new TabIndicator(this.isLoansTab, this.isAnalyticsTab, this.isPersonTab, + this.isShowAllLoans, newIsShowLoaneeInfo); + } + + public TabIndicator setDualPanelView() { + return new TabIndicator(true, false, true, this.isShowAllLoans, this.isShowLoaneeInfo); + } + + /** + * Returns a string representation of the TabIndicator. + * + * @return String representation of the TabIndicator. + */ + public String toString() { + return "Loans: " + isLoansTab + "\n" + + " Analytics: " + isAnalyticsTab + "\n" + + " Person: " + isPersonTab + "\n" + + " ShowAllLoans: " + isShowAllLoans + "\n" + + " ShowLoaneeInfo: " + isShowLoaneeInfo + "\n"; + } + + +} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..3e6d09a6b7b 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,9 +1,12 @@ package seedu.address.model.util; +import java.math.BigDecimal; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; +import seedu.address.commons.util.DateUtil; +import seedu.address.logic.commands.LinkLoanCommand; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Address; @@ -13,15 +16,19 @@ import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; + /** * Contains utility methods for populating {@code AddressBook} with sample data. */ public class SampleDataUtil { - public static Person[] getSamplePersons() { - return new Person[] { + private static Person alex = 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")), + getTagSet("friends")); + + public static Person[] getSamplePersons() { + return new Person[] { + alex, 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")), @@ -40,11 +47,29 @@ public static Person[] getSamplePersons() { }; } + public static LinkLoanCommand.LinkLoanDescriptor[] getSampleLoans() { + LinkLoanCommand.LinkLoanDescriptor loanDescriptor; + try { + loanDescriptor = new LinkLoanCommand.LinkLoanDescriptor(BigDecimal.valueOf(100), + DateUtil.parse("2021-10-10"), + DateUtil.parse("2021-12-10")); + return new LinkLoanCommand.LinkLoanDescriptor[] { + loanDescriptor + }; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + public static ReadOnlyAddressBook getSampleAddressBook() { AddressBook sampleAb = new AddressBook(); for (Person samplePerson : getSamplePersons()) { sampleAb.addPerson(samplePerson); } + for (LinkLoanCommand.LinkLoanDescriptor loanDescriptor : getSampleLoans()) { + sampleAb.addLoan(loanDescriptor, alex); + } return sampleAb; } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedLoan.java b/src/main/java/seedu/address/storage/JsonAdaptedLoan.java new file mode 100644 index 00000000000..7e92a66c215 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedLoan.java @@ -0,0 +1,68 @@ +package seedu.address.storage; + +import java.math.BigDecimal; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.DateUtil; +import seedu.address.model.person.Loan; + +/** + * Jackson-friendly version of {@link Loan}. + */ +public class JsonAdaptedLoan { + + private final BigDecimal value; + private final String startDate; + private final String returnDate; + private final int id; + private final boolean isReturned; + private final JsonAdaptedPerson assignee; + + /** + * Constructs a {@code JsonAdaptedLoan} with the given loan details. + */ + @JsonCreator + public JsonAdaptedLoan(@JsonProperty("value") BigDecimal value, @JsonProperty("startDate") String startDate, + @JsonProperty("returnDate") String returnDate, @JsonProperty("id") int id, + @JsonProperty("isReturned") boolean isReturned, + @JsonProperty("assignee") JsonAdaptedPerson assignee) { + this.value = value; + this.startDate = startDate; + this.returnDate = returnDate; + this.id = id; + this.isReturned = isReturned; + this.assignee = assignee; + } + + /** + * Converts a given {@code Loan} into this class for Jackson use. + */ + public JsonAdaptedLoan(Loan source) { + value = source.getValue(); + startDate = DateUtil.format(source.getStartDate()); + returnDate = DateUtil.format(source.getReturnDate()); + id = source.getId(); + isReturned = source.isReturned(); + assignee = new JsonAdaptedPerson(source.getAssignee()); + } + + /** + * Converts this Jackson-friendly adapted loan object into the model's {@code Loan} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted loan. + */ + public Loan toModelType() throws IllegalValueException { + if (!Loan.isValidValue(value)) { + throw new IllegalValueException(Loan.VALUE_CONSTRAINTS); + } + if (!Loan.isValidDates(DateUtil.parse(startDate), DateUtil.parse(returnDate))) { + throw new IllegalValueException(Loan.DATE_CONSTRAINTS); + } + return new Loan(id, value, DateUtil.parse(startDate), DateUtil.parse(returnDate), isReturned, + assignee.toModelType()); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..fe3aaa9a1c4 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -30,6 +30,7 @@ class JsonAdaptedPerson { private final String address; private final List tags = new ArrayList<>(); + /** * Constructs a {@code JsonAdaptedPerson} with the given person details. */ diff --git a/src/main/java/seedu/address/storage/JsonAdaptedUniqueLoanList.java b/src/main/java/seedu/address/storage/JsonAdaptedUniqueLoanList.java new file mode 100644 index 00000000000..8a834a5ef0a --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedUniqueLoanList.java @@ -0,0 +1,73 @@ +package seedu.address.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Loan; +import seedu.address.model.person.UniqueLoanList; + +/** + * Jackson-friendly version of {@link UniqueLoanList}. + */ +public class JsonAdaptedUniqueLoanList { + + public static final String MISSING_MESSAGE = "UniqueLoanList's loans field is missing!"; + + private final List loans; + + /** + * Constructs a {@code JsonAdaptedUniqueLoanList} with the given loan details. + */ + @JsonCreator + public JsonAdaptedUniqueLoanList(@JsonProperty("loans") List loans) { + this.loans = loans; + } + + /** + * Converts a given {@code UniqueLoanList} into this class for Jackson use. + */ + public JsonAdaptedUniqueLoanList(UniqueLoanList source) { + if (source != null) { + loans = source.getLoanList().stream() + .map(JsonAdaptedLoan::new) + .collect(Collectors.toList()); + } else { + loans = null; + } + } + + /** + * Factory method to create a new instance of JsonAdaptedUniqueLoanList + * to disambiguate the constructor for null values. + */ + public static JsonAdaptedUniqueLoanList factory(UniqueLoanList source) { + return new JsonAdaptedUniqueLoanList(source); + } + + /** + * Converts this Jackson-friendly adapted loan object into the model's {@code UniqueLoanList} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted loan. + */ + public UniqueLoanList toModelType() throws IllegalValueException { + if (loans == null) { + throw new IllegalValueException(MISSING_MESSAGE); + } + + ArrayList loanList = new ArrayList<>(); + for (JsonAdaptedLoan loan : loans) { + loanList.add(loan.toModelType()); + } + UniqueLoanList uniqueLoanList = new UniqueLoanList(); + + uniqueLoanList.setLoans(loanList); + + return uniqueLoanList; + } + +} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..792887b0cf2 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -11,6 +11,7 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.person.Loan; import seedu.address.model.person.Person; /** @@ -20,15 +21,20 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_LOAN = "Loans list contains duplicate loan id(s)."; + public static final String MESSAGE_ORPHAN_LOAN = "Loans list contains loan(s) with no associated person."; private final List persons = new ArrayList<>(); + private final List loans = new ArrayList<>(); /** * Constructs a {@code JsonSerializableAddressBook} with the given persons. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { + public JsonSerializableAddressBook(@JsonProperty("persons") List persons, + @JsonProperty("loans") List loans) { this.persons.addAll(persons); + this.loans.addAll(loans); } /** @@ -38,6 +44,7 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List { + private static final String FXML = "AnalyticsPanel.fxml"; + + @FXML + private PieChart reliabilityChart; + + @FXML + private PieChart impactChart; + + @FXML + private PieChart urgencyChart; + + @FXML + private Label reliabilityIndex; + + @FXML + private Label impactIndex; + + @FXML + private Label urgencyIndex; + + /** + * Creates a {@code AnalyticsPanel} with the given {@code ObjectProperty}. + */ + public AnalyticsPanel(ObjectProperty dashboardData) { + super(FXML); + initializeCharts(); + reliabilityChart.setData(FXCollections.observableArrayList()); + dashboardData.addListener((observable, oldValue, newValue) -> { + updateChart(newValue); + }); + } + + private void initializeCharts() { + reliabilityChart.setStartAngle(90); + impactChart.setStartAngle(90); + urgencyChart.setStartAngle(90); + reliabilityChart.setLabelsVisible(false); + impactChart.setLabelsVisible(false); + urgencyChart.setLabelsVisible(false); + reliabilityChart.setLegendVisible(false); + impactChart.setLegendVisible(false); + urgencyChart.setLegendVisible(false); + } + + private void updateReliability(DashboardData data) { + Analytics analytics = data.getAnalytics(); + if (analytics.getNumActiveLoans() == 0) { + reliabilityIndex.setText("No active loans to analyze"); + reliabilityChart.setVisible(false); + return; + } + reliabilityChart.setVisible(true); + ObservableList reliabilityData = FXCollections.observableArrayList( + new PieChart.Data("Reliability Index", analytics.getPropOverdueLoans()), + new PieChart.Data("", 1 - analytics.getPropOverdueLoans()) + ); + reliabilityChart.setData(reliabilityData); + reliabilityIndex.setText(String.format("%.2f", (1 - analytics.getPropOverdueLoans()) * 100) + "%"); + + } + + private void updateImpact(DashboardData data) { + if (data.getMaxLoanValue().compareTo(BigDecimal.ZERO) == 0) { + impactIndex.setText("No loans to analyze"); + impactChart.setVisible(false); + return; + } + impactChart.setVisible(true); + ObservableList impactData = FXCollections.observableArrayList( + new PieChart.Data("Impact Index", data.getImpactIndex().doubleValue()), + new PieChart.Data("", 1 - data.getImpactIndex().doubleValue()) + ); + impactChart.setData(impactData); + impactIndex.setText(String.format("%.2f", data.getImpactIndex().doubleValue() * 100) + "%"); + } + + private void updateUrgency(DashboardData data) { + if (data.getUrgencyIndex() == null) { + urgencyIndex.setText("No due loans to analyze"); + urgencyChart.setVisible(false); + return; + } + urgencyChart.setVisible(true); + ObservableList urgencyData = FXCollections.observableArrayList( + new PieChart.Data("Urgency Index", data.getUrgencyIndex()), + new PieChart.Data("", 1 - data.getUrgencyIndex()) + ); + urgencyChart.setData(urgencyData); + urgencyIndex.setText(String.format("%.2f", data.getUrgencyIndex() * 100) + "%"); + } + + private void updateChart(DashboardData data) { + updateReliability(data); + updateImpact(data); + updateUrgency(data); + } +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..c6f693fe9a8 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-w13-1.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/LoanCard.java b/src/main/java/seedu/address/ui/LoanCard.java new file mode 100644 index 00000000000..1708d01f6f3 --- /dev/null +++ b/src/main/java/seedu/address/ui/LoanCard.java @@ -0,0 +1,73 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import seedu.address.commons.util.DateUtil; +import seedu.address.model.person.Loan; + +/** + * An UI component that displays information of a {@code Loan}. + */ +public class LoanCard extends UiPart { + private static final String FXML = "LoanListCard.fxml"; + private static final String DEFAULT_LOAN_PREFIX = "Loan No. "; + private static final String DEFAULT_AMOUNT_PREFIX = "Amount: "; + private static final String DEFAULT_START_DATE_PREFIX = "Start Date: "; + private static final String DEFAULT_END_DATE_PREFIX = "End Date: "; + + private static final String DEFAULT_RETURNED_STATUS_PREFIX = "Returned: "; + + /** + * 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 Loan loan; + + @javafx.fxml.FXML + private HBox cardPane; + @FXML + private Label name; + @FXML + private Label amount; + @FXML + private Label startDate; + @FXML + private Label endDate; + + @FXML + private Label returned; + + @FXML + private Label loanee; + + @FXML + private VBox loanCard; + + /** + * Creates a {@code LoanCard} with the given {@code Loan} and index to display. + */ + public LoanCard(Loan loan, int displayedIndex, boolean showLoanee) { + super(FXML); + this.loan = loan; + name.setText(DEFAULT_LOAN_PREFIX + displayedIndex); + amount.setText(DEFAULT_AMOUNT_PREFIX + String.valueOf(loan.getValue())); + startDate.setText(DEFAULT_START_DATE_PREFIX + DateUtil.format(loan.getStartDate())); + endDate.setText(DEFAULT_END_DATE_PREFIX + DateUtil.format(loan.getReturnDate())); + returned.setText(DEFAULT_RETURNED_STATUS_PREFIX + (loan.isReturned() ? "Yes" : "No")); + if (showLoanee) { + loanee.setText("Loanee: " + loan.getAssignee().getName()); + loanee.setVisible(true); + loanee.setManaged(true); + } else { + loanee.setVisible(false); + loanee.setManaged(false); + } + } +} diff --git a/src/main/java/seedu/address/ui/LoanListPanel.java b/src/main/java/seedu/address/ui/LoanListPanel.java new file mode 100644 index 00000000000..81eeeefce0f --- /dev/null +++ b/src/main/java/seedu/address/ui/LoanListPanel.java @@ -0,0 +1,56 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.beans.property.ObjectProperty; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.person.Loan; +import seedu.address.model.tabindicator.TabIndicator; + +/** + * Panel containing the list of persons. + */ +public class LoanListPanel extends UiPart { + private static final String FXML = "LoanListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(LoanListPanel.class); + + @FXML + private ListView loanListView; + + // private final BooleanProperty isShowLoaneeInfo; + private final ObjectProperty tabIndicator; + + /** + * Creates a {@code PersonListPanel} with the given {@code ObservableList}. + */ + public LoanListPanel(ObservableList loanList, ObjectProperty tabIndicator) { + super(FXML); + loanListView.setItems(loanList); + loanListView.setCellFactory(listView -> new LoanListViewCell()); + this.tabIndicator = tabIndicator; + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. + */ + class LoanListViewCell extends ListCell { + @Override + protected void updateItem(Loan loan, boolean empty) { + super.updateItem(loan, empty); + + if (empty || loan == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new LoanCard(loan, getIndex() + 1, + tabIndicator.getValue().getIsShowLoaneeInfo()).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..08fa4cb18f5 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -2,13 +2,16 @@ import java.util.logging.Logger; +import javafx.beans.property.ObjectProperty; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.stage.Stage; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; @@ -16,6 +19,7 @@ import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tabindicator.TabIndicator; /** * The Main Window. Provides the basic application layout containing @@ -50,6 +54,25 @@ public class MainWindow extends UiPart { @FXML private StackPane statusbarPlaceholder; + @FXML + private LoanListPanel loanListPanel; + + @FXML + private StackPane loanListPanelPlaceholder; + + @FXML + private AnalyticsPanel analyticsPanel; + + @FXML + private StackPane analyticsPanelPlaceholder; + @FXML + private VBox loanList; + @FXML + private VBox analytics; + @FXML + private VBox personList; + private ObjectProperty tabIndicator; + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ @@ -78,6 +101,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,11 +134,19 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { + initializeLocalListeners(); + // Initial value of isLoansTab is false by default + assert (!this.tabIndicator.getValue().getIsLoansTab()); + // Initial value of isAnalyticsTab is false by default + assert (!this.tabIndicator.getValue().getIsAnalyticsTab()); personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + loanListPanel = new LoanListPanel(logic.getSortedLoanList(), this.tabIndicator); + analyticsPanel = new AnalyticsPanel(logic.getAnalytics()); + personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); + initializePlaceholderSettings(); StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); @@ -123,6 +155,88 @@ void fillInnerParts() { commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); } + private void initializePlaceholderSettings() { + VBox.setVgrow(personList, Priority.ALWAYS); + VBox.setVgrow(loanList, Priority.NEVER); + VBox.setVgrow(analytics, Priority.NEVER); + } + + private void initializeLocalListeners() { + this.tabIndicator = logic.getTabIndicator(); + + this.tabIndicator.addListener((observable, oldValue, newValue) -> { + toggleTabs(); + }); + + + } + + private void activateDualPanelView() { + clearAllPlaceholders(); + VBox.setVgrow(personList, Priority.ALWAYS); + VBox.setVgrow(loanList, Priority.NEVER); + VBox.setVgrow(analytics, Priority.NEVER); + personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + VBox.setVgrow(loanList, Priority.ALWAYS); + loanListPanelPlaceholder.getChildren().add(loanListPanel.getRoot()); + VBox.setVgrow(analytics, Priority.NEVER); + personListPanelPlaceholder.setMaxHeight(105); + personListPanelPlaceholder.setMinHeight(105); + VBox.setVgrow(personList, Priority.NEVER); + } + + private void activatePersonListOnlyView() { + clearAllPlaceholders(); + personListPanelPlaceholder.setMaxHeight(Double.POSITIVE_INFINITY); + VBox.setVgrow(personList, Priority.ALWAYS); + VBox.setVgrow(loanList, Priority.NEVER); + VBox.setVgrow(analytics, Priority.NEVER); + personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + } + + private void activateAnalyticsView() { + clearAllPlaceholders(); + VBox.setVgrow(analytics, Priority.ALWAYS); + VBox.setVgrow(personList, Priority.NEVER); + VBox.setVgrow(loanList, Priority.NEVER); + personListPanelPlaceholder.setMinHeight(0); + analyticsPanelPlaceholder.getChildren().add(analyticsPanel.getRoot()); + } + + private void activateLoanListOnlyView() { + clearAllPlaceholders(); + VBox.setVgrow(loanList, Priority.ALWAYS); + VBox.setVgrow(personList, Priority.NEVER); + VBox.setVgrow(analytics, Priority.NEVER); + loanListPanelPlaceholder.getChildren().add(loanListPanel.getRoot()); + personListPanelPlaceholder.setMinHeight(0); + } + + private void toggleTabs() { + // At most one of these can be active at a time + assert (!(this.tabIndicator.getValue().getIsLoansTab() && this.tabIndicator.getValue().getIsAnalyticsTab())); + + if (this.tabIndicator.getValue().getIsPersonTab() && this.tabIndicator.getValue().getIsLoansTab()) { + activateDualPanelView(); + } else if (this.tabIndicator.getValue().getIsPersonTab()) { + activatePersonListOnlyView(); + } else if (this.tabIndicator.getValue().getIsLoansTab()) { + activateLoanListOnlyView(); + } else { + activateAnalyticsView(); + } + } + + private void clearAllPlaceholders() { + personListPanelPlaceholder.getChildren().clear(); + VBox.setVgrow(personList, Priority.NEVER); + loanListPanelPlaceholder.getChildren().clear(); + VBox.setVgrow(loanList, Priority.NEVER); + analyticsPanelPlaceholder.getChildren().clear(); + VBox.setVgrow(analytics, Priority.NEVER); + } + + /** * Sets the default size based on {@code guiSettings}. */ diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index f4c501a897b..cd52e96c8e4 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -16,7 +16,6 @@ public class PersonListPanel extends UiPart { private static final String FXML = "PersonListPanel.fxml"; private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - @FXML private ListView personListView; diff --git a/src/main/resources/view/AnalyticsPanel.fxml b/src/main/resources/view/AnalyticsPanel.fxml new file mode 100644 index 00000000000..a27022cdd9f --- /dev/null +++ b/src/main/resources/view/AnalyticsPanel.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/LoanListCard.fxml b/src/main/resources/view/LoanListCard.fxml new file mode 100644 index 00000000000..0e585cc87b7 --- /dev/null +++ b/src/main/resources/view/LoanListCard.fxml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/LoanListPanel.fxml b/src/main/resources/view/LoanListPanel.fxml new file mode 100644 index 00000000000..879ddf2ec21 --- /dev/null +++ b/src/main/resources/view/LoanListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..e9cbe8c1af8 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -6,55 +6,60 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + title="LoanGuardPro" minWidth="450" minHeight="600" onCloseRequest="#handleExit"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicateLoanAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicateLoanAddressBook.json new file mode 100644 index 00000000000..c10733d4e4e --- /dev/null +++ b/src/test/data/JsonSerializableAddressBookTest/duplicateLoanAddressBook.json @@ -0,0 +1,42 @@ +{ + "persons" : [ { + "name" : "Alex Yeet", + "phone" : "87438807", + "email" : "alexyeoh@example.com", + "address" : "Blk 30 Geylang Street 29, #06-40", + "tags" : [ "friends" ] + }, { + "name" : "Bernice Yu", + "phone" : "99272758", + "email" : "berniceyu@example.com", + "address" : "Blk 30 Lorong 3 Serangoon Gardens, #07-18", + "tags" : [ "colleagues", "friends" ] + } ], + "loans" : [ { + "value" : 200, + "startDate" : "2024-02-02", + "returnDate" : "2024-04-09", + "id" : 2, + "isReturned" : false, + "assignee" : { + "name" : "Alex Yeet", + "phone" : "87438807", + "email" : "alexyeoh@example.com", + "address" : "Blk 30 Geylang Street 29, #06-40", + "tags" : [ "friends" ] + } + }, { + "value" : 2000, + "startDate" : "2024-01-01", + "returnDate" : "2024-04-11", + "id" : 2, + "isReturned" : true, + "assignee" : { + "name" : "Alex Yeet", + "phone" : "87438807", + "email" : "alexyeoh@example.com", + "address" : "Blk 30 Geylang Street 29, #06-40", + "tags" : [ "friends" ] + } + } ] +} diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json index a7427fe7aa2..fb80ae7c959 100644 --- a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json @@ -9,6 +9,8 @@ "name": "Alice Pauline", "phone": "94351253", "email": "pauline@example.com", - "address": "4th street" - } ] + "address": "4th street", + "tags": [ "friends" ] + } ], + "loans" : [ ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidLoanAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidLoanAddressBook.json new file mode 100644 index 00000000000..2ef38e5073f --- /dev/null +++ b/src/test/data/JsonSerializableAddressBookTest/invalidLoanAddressBook.json @@ -0,0 +1,29 @@ +{ + "persons" : [ { + "name" : "Alex Yeet", + "phone" : "87438807", + "email" : "alexyeoh@example.com", + "address" : "Blk 30 Geylang Street 29, #06-40", + "tags" : [ "friends" ] + }, { + "name" : "Bernice Yu", + "phone" : "99272758", + "email" : "berniceyu@example.com", + "address" : "Blk 30 Lorong 3 Serangoon Gardens, #07-18", + "tags" : [ "colleagues", "friends" ] + } ], + "loans" : [ { + "value" : 200, + "startDate" : "2024-02-02", + "returnDate" : "2024-04-09", + "id" : 2, + "isReturned" : false, + "assignee" : { + "name" : "Bernice Yu", + "phone" : "87438807", + "email" : "alexyeoh@example.com", + "address" : "Blk 30 Geylang Street 29, #06-40", + "tags" : [ "friends" ] + } + } ] +} diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json index ad3f135ae42..6113859e5af 100644 --- a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json @@ -3,6 +3,8 @@ "name": "Hans Muster", "phone": "9482424", "email": "invalid@email!3e", - "address": "4th street" - } ] + "address": "4th street", + "tags": [ ] + } ], + "loans" : [ ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json index 72262099d35..eacfbab4da1 100644 --- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json @@ -1,5 +1,4 @@ { - "_comment": "AddressBook save file which contains the same Person values as in TypicalPersons#getTypicalAddressBook()", "persons" : [ { "name" : "Alice Pauline", "phone" : "94351253", @@ -42,5 +41,45 @@ "email" : "anna@example.com", "address" : "4th street", "tags" : [ ] + } ], + "loans" : [ { + "value" : 100, + "startDate" : "2020-01-01", + "returnDate" : "2020-01-13", + "id" : 1, + "isReturned" : false, + "assignee" : { + "name" : "Alice Pauline", + "phone" : "94351253", + "email" : "alice@example.com", + "address" : "123, Jurong West Ave 6, #08-111", + "tags" : [ "friends" ] + } + }, { + "value" : 200, + "startDate" : "2024-02-01", + "returnDate" : "2024-02-13", + "id" : 2, + "isReturned" : false, + "assignee" : { + "name" : "Benson Meier", + "phone" : "98765432", + "email" : "johnd@example.com", + "address" : "311, Clementi Ave 2, #02-25", + "tags" : [ "owesMoney", "friends" ] + } + }, { + "value" : 300, + "startDate" : "2024-02-13", + "returnDate" : "2024-02-14", + "id" : 3, + "isReturned" : false, + "assignee" : { + "name" : "Carl Kurz", + "phone" : "95352563", + "email" : "heinz@example.com", + "address" : "wall street", + "tags" : [ ] + } } ] } diff --git a/src/test/java/seedu/address/commons/util/JsonUtilTest.java b/src/test/java/seedu/address/commons/util/JsonUtilTest.java index d4907539dee..8d6aac1521b 100644 --- a/src/test/java/seedu/address/commons/util/JsonUtilTest.java +++ b/src/test/java/seedu/address/commons/util/JsonUtilTest.java @@ -38,8 +38,4 @@ public void deserializeObjectFromJsonFile_noExceptionThrown() throws IOException assertEquals(serializableTestClass.getListOfLocalDateTimes(), SerializableTestClass.getListTestValues()); assertEquals(serializableTestClass.getMapOfIntegerToString(), SerializableTestClass.getHashMapTestValues()); } - - //TODO: @Test jsonUtil_readJsonStringToObjectInstance_correctObject() - - //TODO: @Test jsonUtil_writeThenReadObjectToJson_correctObject() } diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java index 90e8253f48e..1133bd16283 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.Test; +import javafx.beans.property.ObjectProperty; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.logic.Messages; @@ -22,7 +23,11 @@ import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.analytics.DashboardData; +import seedu.address.model.person.Analytics; +import seedu.address.model.person.Loan; import seedu.address.model.person.Person; +import seedu.address.model.tabindicator.TabIndicator; import seedu.address.testutil.PersonBuilder; public class AddCommandTest { @@ -148,6 +153,11 @@ public void setPerson(Person target, Person editedPerson) { throw new AssertionError("This method should not be called."); } + @Override + public boolean hasLoan(Loan loan) { + throw new AssertionError("This method should not be called."); + } + @Override public ObservableList getFilteredPersonList() { throw new AssertionError("This method should not be called."); @@ -157,6 +167,87 @@ public ObservableList getFilteredPersonList() { public void updateFilteredPersonList(Predicate predicate) { throw new AssertionError("This method should not be called."); } + + @Override + public ObservableList getSortedLoanList() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredLoanList(Predicate predicate) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void updateFilteredLoanList(Predicate predicate, boolean isShowAllLoans) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setIsLoansTab(Boolean isLoansTab) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void markLoan(Loan loanToMark) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void unmarkLoan(Loan loanToUnmark) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObjectProperty getDashboardData() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setDualPanel() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setIsShowLoaneeInfo(Boolean isShowLoaneeInfo) { + throw new AssertionError("This method should not be called."); + } + + @Override + public ObjectProperty getTabIndicator() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void deleteLoan(Loan loan) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void addLoan(Loan loan) { + throw new AssertionError("This method should not be called."); + } + + @Override + public Loan addLoan(LinkLoanCommand.LinkLoanDescriptor loanDescription, Person assignee) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setIsAnalyticsTab(Boolean isAnalyticsTab) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void setToPersonTab() { + throw new AssertionError("This method should not be called."); + } + + @Override + public void generateDashboardData(Analytics analytics) { + throw new AssertionError("This method should not be called."); + } + } /** diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java index 643a1d08069..922f98ad5ac 100644 --- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java @@ -9,11 +9,15 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.testutil.Assert.assertThrows; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.List; import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.DateUtil; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.AddressBook; import seedu.address.model.Model; @@ -26,6 +30,30 @@ */ public class CommandTestUtil { + public static final BigDecimal VALID_LOAN_VALUE_ONE = new BigDecimal("500.00"); + public static final BigDecimal VALID_LOAN_VALUE_TWO = new BigDecimal("42.67"); + public static final BigDecimal VALID_LOAN_VALUE_THREE = new BigDecimal("0.01"); + public static final Date VALID_LOAN_START_DATE_ONE; + public static final Date VALID_LOAN_START_DATE_TWO; + public static final Date VALID_LOAN_START_DATE_THREE; + + public static final Date VALID_LOAN_RETURN_DATE_ONE; + public static final Date VALID_LOAN_RETURN_DATE_TWO; + public static final Date VALID_LOAN_RETURN_DATE_THREE; + + static { + try { + VALID_LOAN_START_DATE_ONE = DateUtil.parse("2016-08-25"); + VALID_LOAN_START_DATE_TWO = DateUtil.parse("2024-02-29"); + VALID_LOAN_START_DATE_THREE = DateUtil.parse("2027-12-15"); + + VALID_LOAN_RETURN_DATE_ONE = DateUtil.parse("2020-01-07"); + VALID_LOAN_RETURN_DATE_TWO = DateUtil.parse("2024-03-31"); + VALID_LOAN_RETURN_DATE_THREE = DateUtil.parse("2030-06-02"); + } catch (IllegalValueException e) { + throw new RuntimeException(e); + } + } public static final String VALID_NAME_AMY = "Amy Bee"; public static final String VALID_NAME_BOB = "Bob Choo"; public static final String VALID_PHONE_AMY = "11111111"; diff --git a/src/test/java/seedu/address/logic/commands/DeleteLoanCommandTest.java b/src/test/java/seedu/address/logic/commands/DeleteLoanCommandTest.java new file mode 100644 index 00000000000..4206653162e --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/DeleteLoanCommandTest.java @@ -0,0 +1,76 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.testutil.TypicalPersonsWithLoans.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Loan; + +public class DeleteLoanCommandTest { + + private final Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + private final int loanListSize = model.getSortedLoanList().size(); + + private final Loan firstLoan = model.getSortedLoanList().get(0); + + @Test + public void execute_loanDeleted_success() throws CommandException { + assertTrue(model.getSortedLoanList().contains(firstLoan)); + + DeleteLoanCommand deleteLoanCommand = new DeleteLoanCommand(Index.fromOneBased(1)); + CommandResult commandResult = deleteLoanCommand.execute(model); + int newSize = model.getSortedLoanList().size(); + + assertEquals(loanListSize - 1, newSize); + assertFalse(model.getSortedLoanList().contains(firstLoan)); + assertEquals(String.format(DeleteLoanCommand.MESSAGE_SUCCESS, firstLoan), + commandResult.getFeedbackToUser()); + } + + @Test + public void execute_invalidLoanIndex_failure() { + DeleteLoanCommand deleteLoanCommand = new DeleteLoanCommand(Index.fromOneBased(loanListSize + 1)); + assertCommandFailure(deleteLoanCommand, model, String.format(DeleteLoanCommand.MESSAGE_FAILURE_LOAN, + loanListSize + 1)); + } + + @Test + public void equals() { + DeleteLoanCommand deleteLoanFirstCommand = new DeleteLoanCommand(Index.fromOneBased(1)); + DeleteLoanCommand deleteLoanSecondCommand = new DeleteLoanCommand(Index.fromOneBased(2)); + + // same object -> returns true + assertTrue(deleteLoanFirstCommand.equals(deleteLoanFirstCommand)); + + // same values -> returns true + DeleteLoanCommand deleteLoanFirstCommandCopy = new DeleteLoanCommand(Index.fromOneBased(1)); + assertTrue(deleteLoanFirstCommand.equals(deleteLoanFirstCommandCopy)); + + // different types -> returns false + assertFalse(deleteLoanFirstCommand.equals(1)); + + // null -> returns false + assertFalse(deleteLoanFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(deleteLoanFirstCommand.equals(deleteLoanSecondCommand)); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + DeleteLoanCommand deleteLoanCommand = new DeleteLoanCommand(index); + String expected = DeleteLoanCommand.class.getCanonicalName() + "{loanIndex=" + index + "}"; + assertEquals(expected, deleteLoanCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/EditCommandTest.java b/src/test/java/seedu/address/logic/commands/EditCommandTest.java index 469dd97daa7..5d41344d94a 100644 --- a/src/test/java/seedu/address/logic/commands/EditCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/EditCommandTest.java @@ -84,8 +84,6 @@ public void execute_noFieldSpecifiedUnfilteredList_success() { @Test public void execute_filteredList_success() { - showPersonAtIndex(model, INDEX_FIRST_PERSON); - Person personInFilteredList = model.getFilteredPersonList().get(INDEX_FIRST_PERSON.getZeroBased()); Person editedPerson = new PersonBuilder(personInFilteredList).withName(VALID_NAME_BOB).build(); EditCommand editCommand = new EditCommand(INDEX_FIRST_PERSON, diff --git a/src/test/java/seedu/address/logic/commands/EditLoanCommandTest.java b/src/test/java/seedu/address/logic/commands/EditLoanCommandTest.java new file mode 100644 index 00000000000..bc4563a6d39 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/EditLoanCommandTest.java @@ -0,0 +1,159 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_RETURN_DATE_ONE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_RETURN_DATE_THREE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_RETURN_DATE_TWO; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_START_DATE_ONE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_START_DATE_THREE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_START_DATE_TWO; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_VALUE_ONE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_VALUE_THREE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_VALUE_TWO; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.testutil.TypicalPersonsWithLoans.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditLoanCommand.EditLoanDescriptor; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Loan; + +public class EditLoanCommandTest { + + private final Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + private final int loanListSize = model.getSortedLoanList().size(); + + @Test + public void execute_allFieldsSpecified_success() throws CommandException { + EditLoanDescriptor editLoanDescriptor = new EditLoanDescriptor(); + editLoanDescriptor.setValue(VALID_LOAN_VALUE_ONE); + editLoanDescriptor.setStartDate(VALID_LOAN_START_DATE_ONE); + editLoanDescriptor.setReturnDate(VALID_LOAN_RETURN_DATE_ONE); + + EditLoanCommand editLoanCommand = new EditLoanCommand(editLoanDescriptor, Index.fromOneBased(1)); + CommandResult commandResult = editLoanCommand.execute(model); + // Note that as VALID_LOAN_START_DATE_ONE is before all the other dates, + // the edited loan is still first in the sorted loan list + Loan editedLoan = model.getSortedLoanList().get(0); + + assertEquals(editedLoan.getValue(), VALID_LOAN_VALUE_ONE); + assertEquals(editedLoan.getStartDate(), VALID_LOAN_START_DATE_ONE); + assertEquals(editedLoan.getReturnDate(), VALID_LOAN_RETURN_DATE_ONE); + assertEquals(String.format(EditLoanCommand.MESSAGE_SUCCESS, editedLoan), commandResult.getFeedbackToUser()); + } + + @Test + public void execute_onlyValueSpecified_success() throws CommandException { + EditLoanDescriptor editLoanDescriptor = new EditLoanDescriptor(); + editLoanDescriptor.setValue(VALID_LOAN_VALUE_ONE); + + EditLoanCommand editLoanCommand = new EditLoanCommand(editLoanDescriptor, Index.fromOneBased(1)); + CommandResult commandResult = editLoanCommand.execute(model); + + Loan editedLoan = model.getSortedLoanList().get(0); + + assertEquals(editedLoan.getValue(), VALID_LOAN_VALUE_ONE); + assertEquals(String.format(EditLoanCommand.MESSAGE_SUCCESS, editedLoan), commandResult.getFeedbackToUser()); + } + + @Test + public void execute_onlyStartDateSpecified_success() throws CommandException { + EditLoanDescriptor editLoanDescriptor = new EditLoanDescriptor(); + editLoanDescriptor.setStartDate(VALID_LOAN_START_DATE_ONE); + + EditLoanCommand editLoanCommand = new EditLoanCommand(editLoanDescriptor, Index.fromOneBased(1)); + CommandResult commandResult = editLoanCommand.execute(model); + + Loan editedLoan = model.getSortedLoanList().get(0); + + assertEquals(editedLoan.getStartDate(), VALID_LOAN_START_DATE_ONE); + assertEquals(String.format(EditLoanCommand.MESSAGE_SUCCESS, editedLoan), commandResult.getFeedbackToUser()); + } + + @Test + public void execute_onlyReturnDateSpecified_success() throws CommandException { + EditLoanDescriptor editLoanDescriptor = new EditLoanDescriptor(); + editLoanDescriptor.setReturnDate(VALID_LOAN_RETURN_DATE_ONE); + + EditLoanCommand editLoanCommand = new EditLoanCommand(editLoanDescriptor, Index.fromOneBased(1)); + CommandResult commandResult = editLoanCommand.execute(model); + + Loan editedLoan = model.getSortedLoanList().get(0); + + assertEquals(editedLoan.getReturnDate(), VALID_LOAN_RETURN_DATE_ONE); + assertEquals(String.format(EditLoanCommand.MESSAGE_SUCCESS, editedLoan), commandResult.getFeedbackToUser()); + } + + @Test + public void execute_invalidLoanIndex_failure() { + EditLoanDescriptor editLoanDescriptor = new EditLoanDescriptor(); + editLoanDescriptor.setValue(VALID_LOAN_VALUE_ONE); + editLoanDescriptor.setStartDate(VALID_LOAN_START_DATE_ONE); + editLoanDescriptor.setReturnDate(VALID_LOAN_RETURN_DATE_ONE); + + EditLoanCommand editLoanCommand = new EditLoanCommand(editLoanDescriptor, + Index.fromOneBased(loanListSize + 1)); + assertCommandFailure(editLoanCommand, model, String.format(EditLoanCommand.MESSAGE_FAILURE_LOAN, + loanListSize + 1)); + } + + @Test + public void equals() { + EditLoanDescriptor editLoanDescriptor = new EditLoanDescriptor(); + editLoanDescriptor.setValue(VALID_LOAN_VALUE_ONE); + editLoanDescriptor.setStartDate(VALID_LOAN_START_DATE_ONE); + editLoanDescriptor.setReturnDate(VALID_LOAN_RETURN_DATE_ONE); + EditLoanDescriptor copyDescriptor = new EditLoanDescriptor(); + copyDescriptor.setValue(VALID_LOAN_VALUE_ONE); + copyDescriptor.setStartDate(VALID_LOAN_START_DATE_ONE); + copyDescriptor.setReturnDate(VALID_LOAN_RETURN_DATE_ONE); + + final EditLoanCommand standardCommand = new EditLoanCommand(editLoanDescriptor, + Index.fromOneBased(1)); + EditLoanCommand commandWithSameValues = new EditLoanCommand(copyDescriptor, Index.fromOneBased(1)); + + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new EditLoanCommand(editLoanDescriptor, + Index.fromOneBased(2)))); + + // different descriptor -> returns false + EditLoanDescriptor differentDescriptor = new EditLoanDescriptor(); + differentDescriptor.setValue(VALID_LOAN_VALUE_TWO); + differentDescriptor.setStartDate(VALID_LOAN_START_DATE_TWO); + differentDescriptor.setReturnDate(VALID_LOAN_RETURN_DATE_TWO); + assertFalse(standardCommand.equals(new EditLoanCommand(differentDescriptor, + Index.fromOneBased(1)))); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + EditLoanDescriptor editLoanDescriptor = new EditLoanDescriptor(); + editLoanDescriptor.setValue(VALID_LOAN_VALUE_THREE); + editLoanDescriptor.setStartDate(VALID_LOAN_START_DATE_THREE); + editLoanDescriptor.setReturnDate(VALID_LOAN_RETURN_DATE_THREE); + EditLoanCommand editLoanCommand = new EditLoanCommand(editLoanDescriptor, Index.fromOneBased(1)); + String expected = EditLoanCommand.class.getCanonicalName() + "{loanIndex=" + index + + ", editedDetails=" + editLoanDescriptor + "}"; + assertEquals(expected, editLoanCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/LinkLoanCommandTest.java b/src/test/java/seedu/address/logic/commands/LinkLoanCommandTest.java new file mode 100644 index 00000000000..54404ad4e80 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/LinkLoanCommandTest.java @@ -0,0 +1,102 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_RETURN_DATE_ONE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_RETURN_DATE_THREE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_RETURN_DATE_TWO; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_START_DATE_ONE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_START_DATE_THREE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_START_DATE_TWO; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_VALUE_ONE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_VALUE_THREE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_LOAN_VALUE_TWO; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.LinkLoanCommand.LinkLoanDescriptor; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Loan; +import seedu.address.model.person.Person; + +public class LinkLoanCommandTest { + + private final Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + private final Person firstPerson = model.getFilteredPersonList().get(0); + + private final int personListSize = model.getFilteredPersonList().size(); + + @Test + public void execute_loanLinkedToPerson_success() throws CommandException { + LinkLoanDescriptor linkLoanDescriptor = new LinkLoanDescriptor(VALID_LOAN_VALUE_ONE, + VALID_LOAN_START_DATE_ONE, VALID_LOAN_RETURN_DATE_ONE); + LinkLoanCommand linkLoanCommand = new LinkLoanCommand(linkLoanDescriptor, Index.fromOneBased(1)); + CommandResult commandResult = linkLoanCommand.execute(model); + Loan linkedLoan = model.getSortedLoanList().get(0); + + assertEquals(linkedLoan.getValue(), VALID_LOAN_VALUE_ONE); + assertEquals(linkedLoan.getStartDate(), VALID_LOAN_START_DATE_ONE); + assertEquals(linkedLoan.getReturnDate(), VALID_LOAN_RETURN_DATE_ONE); + assertEquals(linkedLoan.getAssignee(), firstPerson); + assertEquals(String.format(LinkLoanCommand.MESSAGE_SUCCESS, firstPerson.getName(), + linkedLoan), + commandResult.getFeedbackToUser()); + } + + @Test + public void execute_invalidPersonIndex_failure() { + LinkLoanDescriptor linkLoanDescriptor = new LinkLoanDescriptor(VALID_LOAN_VALUE_ONE, + VALID_LOAN_START_DATE_ONE, VALID_LOAN_RETURN_DATE_ONE); + LinkLoanCommand linkLoanCommand = new LinkLoanCommand(linkLoanDescriptor, + Index.fromOneBased(personListSize + 1)); + assertCommandFailure(linkLoanCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void equals() { + LinkLoanDescriptor linkLoanDescriptor = new LinkLoanDescriptor(VALID_LOAN_VALUE_ONE, + VALID_LOAN_START_DATE_ONE, VALID_LOAN_RETURN_DATE_ONE); + LinkLoanDescriptor copyDescriptor = new LinkLoanDescriptor(linkLoanDescriptor); + final LinkLoanCommand standardCommand = new LinkLoanCommand(linkLoanDescriptor, Index.fromOneBased(1)); + LinkLoanCommand commandWithSameValues = new LinkLoanCommand(copyDescriptor, Index.fromOneBased(1)); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new LinkLoanCommand(linkLoanDescriptor, Index.fromOneBased(2)))); + + // different descriptor -> returns false + assertFalse(standardCommand.equals(new LinkLoanCommand(new LinkLoanDescriptor( + VALID_LOAN_VALUE_TWO, + VALID_LOAN_START_DATE_TWO, + VALID_LOAN_RETURN_DATE_TWO), Index.fromOneBased(1)))); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + LinkLoanDescriptor linkLoanDescriptor = new LinkLoanDescriptor(VALID_LOAN_VALUE_THREE, + VALID_LOAN_START_DATE_THREE, VALID_LOAN_RETURN_DATE_THREE); + LinkLoanCommand linkLoanCommand = new LinkLoanCommand(linkLoanDescriptor, Index.fromOneBased(1)); + String expected = LinkLoanCommand.class.getCanonicalName() + "{linkTarget=" + index + + ", loanDescription=" + linkLoanDescriptor + "}"; + assertEquals(expected, linkLoanCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/MarkLoanCommandTest.java b/src/test/java/seedu/address/logic/commands/MarkLoanCommandTest.java new file mode 100644 index 00000000000..17ab88e2fd6 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/MarkLoanCommandTest.java @@ -0,0 +1,78 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.testutil.TypicalPersonsWithLoans.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Loan; + + +/** + * Contains integration tests (interaction with the Model) and unit tests for MarkLoanCommand. + */ +public class MarkLoanCommandTest { + + private final Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + private final int loanListSize = model.getSortedLoanList().size(); + + private final Loan firstLoan = model.getSortedLoanList().get(0); + + @Test + public void execute_markLoanAsReturned_success() throws CommandException { + assertTrue(firstLoan.isActive()); + + MarkLoanCommand markLoanCommand = new MarkLoanCommand(Index.fromOneBased(1)); + CommandResult commandResult = markLoanCommand.execute(model); + + assertTrue(firstLoan.isReturned()); + assertEquals(String.format(MarkLoanCommand.MESSAGE_SUCCESS, firstLoan), + commandResult.getFeedbackToUser()); + } + + @Test + public void execute_invalidLoanIndex_failure() { + MarkLoanCommand markLoanCommand = new MarkLoanCommand(Index.fromOneBased(loanListSize + 1)); + assertCommandFailure(markLoanCommand, model, String.format(MarkLoanCommand.MESSAGE_FAILURE_LOAN, + loanListSize + 1)); + } + + @Test + public void equals() { + MarkLoanCommand markLoanFirstCommand = new MarkLoanCommand(Index.fromOneBased(1)); + MarkLoanCommand markLoanSecondCommand = new MarkLoanCommand(Index.fromOneBased(2)); + + // same object -> returns true + assertTrue(markLoanFirstCommand.equals(markLoanFirstCommand)); + + // same values -> returns true + MarkLoanCommand markLoanFirstCommandCopy = new MarkLoanCommand(Index.fromOneBased(1)); + assertTrue(markLoanFirstCommand.equals(markLoanFirstCommandCopy)); + + // different types -> returns false + assertFalse(markLoanFirstCommand.equals(1)); + + // null -> returns false + assertFalse(markLoanFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(markLoanFirstCommand.equals(markLoanSecondCommand)); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + MarkLoanCommand markLoanCommand = new MarkLoanCommand(index); + String expected = MarkLoanCommand.class.getCanonicalName() + "{loanIndex=" + index + "}"; + assertEquals(expected, markLoanCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/UnmarkLoanCommandTest.java b/src/test/java/seedu/address/logic/commands/UnmarkLoanCommandTest.java new file mode 100644 index 00000000000..dde8c163c5d --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/UnmarkLoanCommandTest.java @@ -0,0 +1,76 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.testutil.TypicalPersonsWithLoans.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Loan; + +public class UnmarkLoanCommandTest { + private final Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + private final int loanListSize = model.getSortedLoanList().size(); + + private final Loan firstLoan = model.getSortedLoanList().get(0); + + @Test + public void execute_unmarkLoanAsReturned_success() throws CommandException { + MarkLoanCommand markLoanCommand = new MarkLoanCommand(Index.fromOneBased(1)); + markLoanCommand.execute(model); + + assertTrue(firstLoan.isReturned()); + + UnmarkLoanCommand unmarkLoanCommand = new UnmarkLoanCommand(Index.fromOneBased(1)); + CommandResult commandResult = unmarkLoanCommand.execute(model); + + assertTrue(firstLoan.isActive()); + assertEquals(String.format(unmarkLoanCommand.MESSAGE_SUCCESS, firstLoan), + commandResult.getFeedbackToUser()); + } + + @Test + public void execute_invalidLoanIndex_failure() { + UnmarkLoanCommand unmarkLoanCommand = new UnmarkLoanCommand(Index.fromOneBased(loanListSize + 1)); + assertCommandFailure(unmarkLoanCommand, model, String.format(unmarkLoanCommand.MESSAGE_FAILURE_LOAN, + loanListSize + 1)); + } + + @Test + public void equals() { + UnmarkLoanCommand unmarkLoanFirstCommand = new UnmarkLoanCommand(Index.fromOneBased(1)); + UnmarkLoanCommand unmarkLoanSecondCommand = new UnmarkLoanCommand(Index.fromOneBased(2)); + + // same object -> returns true + assertTrue(unmarkLoanFirstCommand.equals(unmarkLoanFirstCommand)); + + // same values -> returns true + UnmarkLoanCommand unmarkLoanFirstCommandCopy = new UnmarkLoanCommand(Index.fromOneBased(1)); + assertTrue(unmarkLoanFirstCommand.equals(unmarkLoanFirstCommandCopy)); + + // different types -> returns false + assertFalse(unmarkLoanFirstCommand.equals(1)); + + // null -> returns false + assertFalse(unmarkLoanFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(unmarkLoanFirstCommand.equals(unmarkLoanSecondCommand)); + } + + @Test + public void toStringMethod() { + Index index = Index.fromOneBased(1); + UnmarkLoanCommand unmarkLoanCommand = new UnmarkLoanCommand(index); + String expected = UnmarkLoanCommand.class.getCanonicalName() + "{loanIndex=" + index + "}"; + assertEquals(expected, unmarkLoanCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index 5a1ab3dbc0c..43c830842b9 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -13,15 +13,19 @@ import org.junit.jupiter.api.Test; +import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteLoanCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.MarkLoanCommand; +import seedu.address.logic.commands.UnmarkLoanCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; @@ -88,6 +92,30 @@ public void parseCommand_list() throws Exception { assertTrue(parser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); } + @Test + public void parseCommand_deleteLoan() throws Exception { + DeleteLoanCommand ddlc = new DeleteLoanCommand(Index.fromOneBased(3)); + DeleteLoanCommand ddlc2 = (DeleteLoanCommand) parser.parseCommand(DeleteLoanCommand.COMMAND_WORD + " 3"); + assertEquals(ddlc, ddlc2); + assertTrue(parser.parseCommand(DeleteLoanCommand.COMMAND_WORD + " 3") instanceof DeleteLoanCommand); + } + + @Test + public void parseCommand_markLoan() throws Exception { + MarkLoanCommand mlc = new MarkLoanCommand(Index.fromOneBased(3)); + MarkLoanCommand mlc2 = (MarkLoanCommand) parser.parseCommand(MarkLoanCommand.COMMAND_WORD + " 3"); + assertEquals(mlc, mlc2); + assertTrue(parser.parseCommand(MarkLoanCommand.COMMAND_WORD + " 3") instanceof MarkLoanCommand); + } + + @Test + public void parseCommand_unmarkLoan() throws Exception { + UnmarkLoanCommand mlc = new UnmarkLoanCommand(Index.fromOneBased(3)); + UnmarkLoanCommand mlc2 = (UnmarkLoanCommand) parser.parseCommand(UnmarkLoanCommand.COMMAND_WORD + " 3"); + assertEquals(mlc, mlc2); + assertTrue(parser.parseCommand(UnmarkLoanCommand.COMMAND_WORD + " 3") instanceof UnmarkLoanCommand); + } + @Test public void parseCommand_unrecognisedInput_throwsParseException() { assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), () diff --git a/src/test/java/seedu/address/logic/parser/DeleteLoanParserTest.java b/src/test/java/seedu/address/logic/parser/DeleteLoanParserTest.java new file mode 100644 index 00000000000..7aa2517d1ad --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/DeleteLoanParserTest.java @@ -0,0 +1,39 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.DeleteLoanCommand; + +public class DeleteLoanParserTest { + private DeleteLoanCommandParser parser = new DeleteLoanCommandParser(); + @Test + public void parse_validArgs_returnsMarkLoanCommand() { + // no leading and trailing whitespaces + DeleteLoanCommand expectedMarkLoanCommand = + new DeleteLoanCommand(Index.fromOneBased(1)); + assertParseSuccess(parser, "1", expectedMarkLoanCommand); + + // multiple whitespaces between index + assertParseSuccess(parser, " 1 ", expectedMarkLoanCommand); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + // negative index + assertParseFailure(parser, "-1", String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + DeleteLoanCommand.MESSAGE_USAGE)); + + // zero index + assertParseFailure(parser, "-1", String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + DeleteLoanCommand.MESSAGE_USAGE)); + + // non-integer index + assertParseFailure(parser, "a", String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + DeleteLoanCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/MarkLoanCommandParserTest.java b/src/test/java/seedu/address/logic/parser/MarkLoanCommandParserTest.java new file mode 100644 index 00000000000..deb73a203c6 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/MarkLoanCommandParserTest.java @@ -0,0 +1,42 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.MarkLoanCommand; + + + +public class MarkLoanCommandParserTest { + + private MarkLoanCommandParser parser = new MarkLoanCommandParser(); + @Test + public void parse_validArgs_returnsMarkLoanCommand() { + // no leading and trailing whitespaces + MarkLoanCommand expectedMarkLoanCommand = + new MarkLoanCommand(Index.fromOneBased(1)); + assertParseSuccess(parser, "1", expectedMarkLoanCommand); + + // multiple whitespaces between index + assertParseSuccess(parser, " 1 ", expectedMarkLoanCommand); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + // negative index + assertParseFailure(parser, "-1", String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + MarkLoanCommand.MESSAGE_USAGE)); + + // zero index + assertParseFailure(parser, "-1", String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + MarkLoanCommand.MESSAGE_USAGE)); + + // non-integer index + assertParseFailure(parser, "a", String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + MarkLoanCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/UnmarkLoanCommandParserTest.java b/src/test/java/seedu/address/logic/parser/UnmarkLoanCommandParserTest.java new file mode 100644 index 00000000000..c6525c75338 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/UnmarkLoanCommandParserTest.java @@ -0,0 +1,41 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.UnmarkLoanCommand; + + +public class UnmarkLoanCommandParserTest { + + private UnmarkLoanCommandParser parser = new UnmarkLoanCommandParser(); + @Test + public void parse_validArgs_returnsMarkLoanCommand() { + // no leading and trailing whitespaces + UnmarkLoanCommand expectedMarkLoanCommand = + new UnmarkLoanCommand(Index.fromOneBased(1)); + assertParseSuccess(parser, "1", expectedMarkLoanCommand); + + // multiple whitespaces between index + assertParseSuccess(parser, " 1 ", expectedMarkLoanCommand); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + // negative index + assertParseFailure(parser, "-1", String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + UnmarkLoanCommand.MESSAGE_USAGE)); + + // zero index + assertParseFailure(parser, "-1", String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + UnmarkLoanCommand.MESSAGE_USAGE)); + + // non-integer index + assertParseFailure(parser, "a", String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, + UnmarkLoanCommand.MESSAGE_USAGE)); + } +} diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java index 68c8c5ba4d5..7e6fd39e997 100644 --- a/src/test/java/seedu/address/model/AddressBookTest.java +++ b/src/test/java/seedu/address/model/AddressBookTest.java @@ -18,6 +18,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import seedu.address.model.person.Loan; import seedu.address.model.person.Person; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.testutil.PersonBuilder; @@ -94,6 +95,7 @@ public void toStringMethod() { */ private static class AddressBookStub implements ReadOnlyAddressBook { private final ObservableList persons = FXCollections.observableArrayList(); + private final ObservableList loans = FXCollections.observableArrayList(); AddressBookStub(Collection persons) { this.persons.setAll(persons); @@ -103,6 +105,11 @@ private static class AddressBookStub implements ReadOnlyAddressBook { public ObservableList getPersonList() { return persons; } + + @Override + public ObservableList getLoanList() { + return loans; + } } } diff --git a/src/test/java/seedu/address/model/analytics/DashboardDataTest.java b/src/test/java/seedu/address/model/analytics/DashboardDataTest.java new file mode 100644 index 00000000000..b4307e8bc52 --- /dev/null +++ b/src/test/java/seedu/address/model/analytics/DashboardDataTest.java @@ -0,0 +1,86 @@ +package seedu.address.model.analytics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.math.BigDecimal; +import java.util.Date; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.Analytics; +import seedu.address.model.person.Loan; +import seedu.address.model.person.UniqueLoanList; +import seedu.address.testutil.LoanBuilder; + + +public class DashboardDataTest { + + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new DashboardData(null, + new BigDecimal("100"), new Date())); + } + + @Test + public void urgencyTest() { + Date oneWeekAfterNow = new Date(new Date().getTime() + 604800000); + Date twoWeeksAfterNow = new Date(new Date().getTime() + 1209600000); + Loan l1 = new LoanBuilder().withId(1).withReturnDate(oneWeekAfterNow).build(); + Loan l2 = new LoanBuilder().withId(2).withReturnDate(twoWeeksAfterNow).build(); + Loan l3 = new LoanBuilder().withId(3).withReturnDate(twoWeeksAfterNow).build(); + + UniqueLoanList loanList = new UniqueLoanList(); + loanList.addLoan(l1); + loanList.addLoan(l2); + loanList.addLoan(l3); + + Analytics a1 = Analytics.getAnalytics(loanList.asUnmodifiableObservableList()); + // Earliest return date in whole database is today + // Urgency index should be zero + DashboardData dd1 = new DashboardData(a1, new BigDecimal("100"), new Date()); + assertEquals(0, dd1.getUrgencyIndex()); + + // Earliest return date in whole database is one week after now + // Since l1 has the same due date, urgency should be 100% + DashboardData dd2 = new DashboardData(a1, new BigDecimal("100"), oneWeekAfterNow); + assertEquals(1, dd2.getUrgencyIndex()); + + // Return loan 1 + l1.markAsReturned(); + // Regenerate analytics + Analytics a2 = Analytics.getAnalytics(loanList.asUnmodifiableObservableList()); + // Earliest return date in whole database is 2 week after now + DashboardData dd3 = new DashboardData(a2, new BigDecimal("100"), oneWeekAfterNow); + assertEquals((float) 0.5, dd3.getUrgencyIndex()); + } + + @Test + public void impactTest() { + Date oneWeekBeforeNow = new Date(new Date().getTime() - 604800000); + Loan l1 = new LoanBuilder().withId(1).withValue(new BigDecimal("100")).withReturnDate(oneWeekBeforeNow).build(); + Loan l2 = new LoanBuilder().withId(2).withValue(new BigDecimal("200")).withReturnDate(oneWeekBeforeNow).build(); + Loan l3 = new LoanBuilder().withId(3).withValue(new BigDecimal("300")).withReturnDate(oneWeekBeforeNow).build(); + + UniqueLoanList loanList = new UniqueLoanList(); + loanList.addLoan(l1); + loanList.addLoan(l2); + loanList.addLoan(l3); + + Analytics a1 = Analytics.getAnalytics(loanList.asUnmodifiableObservableList()); + DashboardData dd1 = new DashboardData(a1, new BigDecimal("300"), new Date()); + assertEquals(new BigDecimal("0.67"), dd1.getImpactIndex()); + + // return all loans + l1.markAsReturned(); + l2.markAsReturned(); + l3.markAsReturned(); + + // Regenerate analytics + Analytics a2 = Analytics.getAnalytics(loanList.asUnmodifiableObservableList()); + DashboardData dd2 = new DashboardData(a2, new BigDecimal("300"), new Date()); + // Should not change since returned loans should not affect impact index + assertEquals(new BigDecimal("0.67"), dd2.getImpactIndex()); + + } +} diff --git a/src/test/java/seedu/address/model/person/AnalyticsTest.java b/src/test/java/seedu/address/model/person/AnalyticsTest.java new file mode 100644 index 00000000000..e79f3bd9238 --- /dev/null +++ b/src/test/java/seedu/address/model/person/AnalyticsTest.java @@ -0,0 +1,94 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.LoanBuilder; + +public class AnalyticsTest { + + @Test + public void constructor_null_throwsNullPointerException() { + // Null loan list should throw NullPointerException + assertThrows(NullPointerException.class, () -> Analytics.getAnalytics(null)); + } + + @Test + public void emptyLoanList() { + // Empty loan list should not throw any exceptions + Analytics test = Analytics.getAnalytics(new UniqueLoanList().asUnmodifiableObservableList()); + assertEquals(0, test.getNumActiveLoans()); // No active loans + assertEquals(0, test.getPropOverdueLoans()); // Proportion of overdue loans is 0 (default) + assertEquals(0, test.getPropActiveLoans()); // Proportion of active loans is 0 (default) + assertEquals(BigDecimal.ZERO, test.getAverageLoanValue()); // Average loan value is 0 (default) + assertNull(test.getEarliestReturnDate()); // Earliest loan date is null + } + + @Test + public void singleLoan() { + // Single loan with value 100 + Loan loan = new LoanBuilder().withValue(new BigDecimal("100.00")).build(); + UniqueLoanList loanList = new UniqueLoanList(); + loanList.addLoan(loan); + Analytics test = Analytics.getAnalytics(loanList.asUnmodifiableObservableList()); + assertEquals(1, test.getNumActiveLoans()); // 1 active loan + assertEquals(0, test.getPropOverdueLoans()); // Proportion of overdue loans is 0 (default) + assertEquals(1, test.getPropActiveLoans()); // Proportion of active loans is 1 + assertEquals(new BigDecimal("100.00"), test.getAverageLoanValue()); // Average loan value is 100 + assertEquals(loan.getReturnDate(), test.getEarliestReturnDate()); // return the loan's return date + } + + @Test + public void multipleActiveLoans() { + Loan loan1 = new LoanBuilder().withId(1).withValue(new BigDecimal("100.00")).build(); + Loan loan2 = new LoanBuilder().withId(2).withValue(new BigDecimal("200.00")).build(); + Loan loan3 = new LoanBuilder().withId(3).withValue(new BigDecimal("300.00")).build(); + Loan loan4 = new LoanBuilder().withId(4) + .withValue(new BigDecimal("200.00")).withReturnDate("2010-01-01").build(); // Earliest return date + UniqueLoanList loanList = new UniqueLoanList(); + loanList.addLoan(loan1); + loanList.addLoan(loan2); + loanList.addLoan(loan3); + loanList.addLoan(loan4); + Analytics test = Analytics.getAnalytics(loanList.asUnmodifiableObservableList()); + assertEquals(4, test.getNumActiveLoans()); // 4 active loans + assertEquals(0.25, test.getPropOverdueLoans()); // Proportion of overdue loans is 0.25 + assertEquals(1, test.getPropActiveLoans()); // Proportion of active loans is 1 + assertEquals(new BigDecimal("200.00"), test.getAverageLoanValue()); // Average loan value is 200 + // Earliest return date should NOT be loan4's return date since it is overdue + assertNotEquals(loan4.getReturnDate(), test.getEarliestReturnDate()); + // Earliest return date can be either 1,2 or 3's return date + assertEquals(loan1.getReturnDate(), test.getEarliestReturnDate()); + } + + @Test + public void withInactiveLoans() { + Loan loan1 = new LoanBuilder().withId(1).withValue(new BigDecimal("100.00")) + .withReturnDate("2025-01-01").build(); + Loan loan2 = new LoanBuilder().withId(2).withValue(new BigDecimal("200.00")).build(); + Loan loan3 = new LoanBuilder().withId(3).withValue(new BigDecimal("300.00")).build(); + Loan loan4 = new LoanBuilder().withId(4) + .withValue(new BigDecimal("200.00")).withReturnDate("2010-01-01").build(); + loan4.markAsReturned(); + UniqueLoanList loanList = new UniqueLoanList(); + loanList.addLoan(loan1); + loanList.addLoan(loan2); + loanList.addLoan(loan3); + loanList.addLoan(loan4); + Analytics test = Analytics.getAnalytics(loanList.asUnmodifiableObservableList()); + assertEquals(3, test.getNumActiveLoans()); // 3 active loans + assertEquals(0, test.getPropOverdueLoans()); // Proportion of overdue loans is 0 (loan4 is returned) + assertEquals(0.75, test.getPropActiveLoans()); // Proportion of active loans is 1 + assertEquals(new BigDecimal("200.00"), test.getAverageLoanValue()); // Average loan value is 200 + // Earliest return date must be loan1's return date + assertEquals(loan1.getReturnDate(), test.getEarliestReturnDate()); + } + + +} diff --git a/src/test/java/seedu/address/model/person/LoanTest.java b/src/test/java/seedu/address/model/person/LoanTest.java new file mode 100644 index 00000000000..8bd5c829b9c --- /dev/null +++ b/src/test/java/seedu/address/model/person/LoanTest.java @@ -0,0 +1,81 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.TypicalLoans.ACTIVE_NON_OVERDUE_LOAN; +import static seedu.address.testutil.TypicalLoans.INACTIVE_LOAN; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.util.DateUtil; +import seedu.address.testutil.LoanBuilder; + +public class LoanTest { + + @Test + public void equals() { + // same object -> returns true + assertTrue(ACTIVE_NON_OVERDUE_LOAN.equals(ACTIVE_NON_OVERDUE_LOAN)); + + // null -> returns false + assertFalse(ACTIVE_NON_OVERDUE_LOAN.equals(null)); + + // same name, all other attributes different -> returns true + Loan editedActiveNonOverdueLoan = new LoanBuilder(ACTIVE_NON_OVERDUE_LOAN).withValue(INACTIVE_LOAN.getValue()) + .withStartDate(INACTIVE_LOAN.getStartDate()).withReturnDate(INACTIVE_LOAN.getReturnDate()) + .withIsReturned(INACTIVE_LOAN.isReturned()).withAssignee(INACTIVE_LOAN.getAssignee()).build(); + assertTrue(ACTIVE_NON_OVERDUE_LOAN.equals(editedActiveNonOverdueLoan)); + + // different name, all other attributes same -> returns false + editedActiveNonOverdueLoan = new LoanBuilder(ACTIVE_NON_OVERDUE_LOAN).withId(6969).build(); + assertFalse(ACTIVE_NON_OVERDUE_LOAN.equals(editedActiveNonOverdueLoan)); + } + + @Test + public void isValidValue() { + // value is zero -> returns false + assertFalse(Loan.isValidValue(BigDecimal.ZERO)); + + // value is negative -> returns false + assertFalse(Loan.isValidValue(new BigDecimal("-1.00"))); + + // value is positive -> returns true + assertTrue(Loan.isValidValue(new BigDecimal("1.00"))); + + // value is very large number -> returns true + assertTrue(Loan.isValidValue(new BigDecimal("11235638206.00"))); + } + + @Test + public void markAsReturned() { + Loan loanCopy = new LoanBuilder(ACTIVE_NON_OVERDUE_LOAN).build(); + loanCopy.markAsReturned(); + assertTrue(loanCopy.isReturned()); + } + + @Test + public void unmarkAsReturned() { + Loan loanCopy = new LoanBuilder(INACTIVE_LOAN).build(); + loanCopy.unmarkAsReturned(); + assertFalse(loanCopy.isReturned()); + } + + @Test + public void toStringMethod() { + String expected = String.format("$%.2f, %s, %s", + ACTIVE_NON_OVERDUE_LOAN.getValue(), + DateUtil.format(ACTIVE_NON_OVERDUE_LOAN.getStartDate()), + DateUtil.format(ACTIVE_NON_OVERDUE_LOAN.getReturnDate())); + assertEquals(expected, ACTIVE_NON_OVERDUE_LOAN.toString()); + + expected = String.format("$%.2f, %s, %s (Returned)", + INACTIVE_LOAN.getValue(), + DateUtil.format(INACTIVE_LOAN.getStartDate()), + DateUtil.format(INACTIVE_LOAN.getReturnDate())); + assertEquals(expected, INACTIVE_LOAN.toString()); + } + +} diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java index 83b11331cdb..2bf45985768 100644 --- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java +++ b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java @@ -41,14 +41,16 @@ public void toModelType_validPersonDetails_returnsPerson() throws Exception { @Test public void toModelType_invalidName_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, + VALID_ADDRESS, VALID_TAGS); String expectedMessage = Name.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullName_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_TAGS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -56,14 +58,16 @@ public void toModelType_nullName_throwsIllegalValueException() { @Test public void toModelType_invalidPhone_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_TAGS); String expectedMessage = Phone.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullPhone_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, + VALID_TAGS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -71,14 +75,16 @@ public void toModelType_nullPhone_throwsIllegalValueException() { @Test public void toModelType_invalidEmail_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, + VALID_TAGS); String expectedMessage = Email.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullEmail_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, + VALID_TAGS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -86,14 +92,16 @@ public void toModelType_nullEmail_throwsIllegalValueException() { @Test public void toModelType_invalidAddress_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, + VALID_TAGS); String expectedMessage = Address.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, + VALID_TAGS); String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -103,8 +111,8 @@ public void toModelType_invalidTags_throwsIllegalValueException() { List invalidTags = new ArrayList<>(VALID_TAGS); invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + invalidTags); assertThrows(IllegalValueException.class, person::toModelType); } - } diff --git a/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java b/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java index 188c9058d20..9fcecb8deee 100644 --- a/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java +++ b/src/test/java/seedu/address/storage/JsonSerializableAddressBookTest.java @@ -1,6 +1,5 @@ package seedu.address.storage; -import static org.junit.jupiter.api.Assertions.assertEquals; import static seedu.address.testutil.Assert.assertThrows; import java.nio.file.Path; @@ -11,37 +10,53 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.commons.util.JsonUtil; import seedu.address.model.AddressBook; -import seedu.address.testutil.TypicalPersons; public class JsonSerializableAddressBookTest { private static final Path TEST_DATA_FOLDER = Paths.get("src", "test", "data", "JsonSerializableAddressBookTest"); private static final Path TYPICAL_PERSONS_FILE = TEST_DATA_FOLDER.resolve("typicalPersonsAddressBook.json"); private static final Path INVALID_PERSON_FILE = TEST_DATA_FOLDER.resolve("invalidPersonAddressBook.json"); + private static final Path INVALID_LOAN_FILE = TEST_DATA_FOLDER.resolve("invalidLoanAddressBook.json"); private static final Path DUPLICATE_PERSON_FILE = TEST_DATA_FOLDER.resolve("duplicatePersonAddressBook.json"); + private static final Path DUPLICATE_LOAN_FILE = TEST_DATA_FOLDER.resolve("duplicateLoanAddressBook.json"); @Test public void toModelType_typicalPersonsFile_success() throws Exception { - JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(TYPICAL_PERSONS_FILE, - JsonSerializableAddressBook.class).get(); - AddressBook addressBookFromFile = dataFromFile.toModelType(); - AddressBook typicalPersonsAddressBook = TypicalPersons.getTypicalAddressBook(); - assertEquals(addressBookFromFile, typicalPersonsAddressBook); + JsonSerializableAddressBook ab = + JsonUtil.readJsonFile(TYPICAL_PERSONS_FILE, JsonSerializableAddressBook.class).get(); + AddressBook addressBook = ab.toModelType(); } @Test public void toModelType_invalidPersonFile_throwsIllegalValueException() throws Exception { - JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(INVALID_PERSON_FILE, - JsonSerializableAddressBook.class).get(); - assertThrows(IllegalValueException.class, dataFromFile::toModelType); + JsonSerializableAddressBook ab = + JsonUtil.readJsonFile(INVALID_PERSON_FILE, JsonSerializableAddressBook.class).get(); + assertThrows(IllegalValueException.class, ab::toModelType); } @Test public void toModelType_duplicatePersons_throwsIllegalValueException() throws Exception { - JsonSerializableAddressBook dataFromFile = JsonUtil.readJsonFile(DUPLICATE_PERSON_FILE, - JsonSerializableAddressBook.class).get(); - assertThrows(IllegalValueException.class, JsonSerializableAddressBook.MESSAGE_DUPLICATE_PERSON, - dataFromFile::toModelType); + JsonSerializableAddressBook ab = + JsonUtil.readJsonFile(DUPLICATE_PERSON_FILE, JsonSerializableAddressBook.class).get(); + assertThrows(IllegalValueException.class, + JsonSerializableAddressBook.MESSAGE_DUPLICATE_PERSON, + ab::toModelType); + } + + @Test + public void toModelType_invalidLoanFile_throwsIllegalValueException() throws Exception { + JsonSerializableAddressBook ab = + JsonUtil.readJsonFile(INVALID_LOAN_FILE, JsonSerializableAddressBook.class).get(); + assertThrows(IllegalValueException.class, ab::toModelType); + } + + @Test + public void toModelType_duplicateLoans_throwsIllegalValueException() throws Exception { + JsonSerializableAddressBook ab = + JsonUtil.readJsonFile(DUPLICATE_LOAN_FILE, JsonSerializableAddressBook.class).get(); + assertThrows(IllegalValueException.class, + JsonSerializableAddressBook.MESSAGE_DUPLICATE_LOAN, + ab::toModelType); } } diff --git a/src/test/java/seedu/address/testutil/LoanBuilder.java b/src/test/java/seedu/address/testutil/LoanBuilder.java new file mode 100644 index 00000000000..8fc18b0bb86 --- /dev/null +++ b/src/test/java/seedu/address/testutil/LoanBuilder.java @@ -0,0 +1,133 @@ +package seedu.address.testutil; + +import java.math.BigDecimal; +import java.util.Date; + +import seedu.address.commons.util.DateUtil; +import seedu.address.model.person.Loan; +import seedu.address.model.person.Person; + +/** + * A utility class to help with building Person objects. + */ +public class LoanBuilder { + + public static final int DEFAULT_ID = 9999; + public static final BigDecimal DEFAULT_VALUE = new BigDecimal("100.00"); + public static final String DEFAULT_START_DATE = "2020-01-01"; + public static final String DEFAULT_RETURN_DATE = "2030-02-01"; + public static final boolean DEFAULT_IS_RETURNED = false; + public static final Person DEFAULT_ASSIGNEE = new PersonBuilder().build(); + + private int id; + private BigDecimal value; + private Date startDate; + private Date returnDate; + private boolean isReturned; + private Person assignee; + + /** + * Creates a {@code LoanBuilder} with the default details. + */ + public LoanBuilder() { + id = DEFAULT_ID; + value = DEFAULT_VALUE; + isReturned = DEFAULT_IS_RETURNED; + assignee = DEFAULT_ASSIGNEE; + try { + startDate = DateUtil.parse(DEFAULT_START_DATE); + returnDate = DateUtil.parse(DEFAULT_RETURN_DATE); + } catch (Exception e) { + System.out.println("THIS SHOULD NOT HAPPEN"); + } + } + + /** + * Initializes the LoanBuilder with the data of {@code loanToCopy}. + */ + public LoanBuilder(Loan loan) { + id = loan.getId(); + value = loan.getValue(); + startDate = loan.getStartDate(); + returnDate = loan.getReturnDate(); + isReturned = loan.isReturned(); + assignee = loan.getAssignee(); + } + + /** + * Sets the {@code id} of the {@code Loan} that we are building. + */ + public LoanBuilder withId(int id) { + this.id = id; + return this; + } + + /** + * Parses the {@code value} of the {@code Loan} that we are building. + */ + public LoanBuilder withValue(BigDecimal value) { + this.value = value; + return this; + } + + /** + * Sets the {@code startDate} of the {@code Loan} that we are building. + */ + public LoanBuilder withStartDate(String startDate) { + try { + this.startDate = DateUtil.parse(startDate); + } catch (Exception e) { + System.out.println("THIS SHOULD NOT HAPPEN"); + } + return this; + } + + /** + * Sets the {@code startDate} of the {@code Loan} that we are building. + */ + public LoanBuilder withStartDate(Date startDate) { + this.startDate = startDate; + return this; + } + + /** + * Sets the {@code returnDate} of the {@code Loan} that we are building. + */ + public LoanBuilder withReturnDate(String returnDate) { + try { + this.returnDate = DateUtil.parse(returnDate); + } catch (Exception e) { + System.out.println("THIS SHOULD NOT HAPPEN"); + } + return this; + } + + /** + * Sets the {@code returnDate} of the {@code Loan} that we are building. + */ + public LoanBuilder withReturnDate(Date returnDate) { + this.returnDate = returnDate; + return this; + } + + /** + * Sets the {@code isReturned} of the {@code Loan} that we are building. + */ + public LoanBuilder withIsReturned(boolean isReturned) { + this.isReturned = isReturned; + return this; + } + + /** + * Sets the {@code assignee} of the {@code Loan} that we are building. + */ + public LoanBuilder withAssignee(Person assignee) { + this.assignee = assignee; + return this; + } + + public Loan build() { + return new Loan(id, value, startDate, returnDate, isReturned, assignee); + } + +} diff --git a/src/test/java/seedu/address/testutil/TypicalLoans.java b/src/test/java/seedu/address/testutil/TypicalLoans.java new file mode 100644 index 00000000000..32bf2c9aafb --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalLoans.java @@ -0,0 +1,32 @@ +package seedu.address.testutil; + +import static seedu.address.testutil.TypicalPersons.BOB; +import static seedu.address.testutil.TypicalPersons.CARL; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.model.person.Loan; + +/** + * A utility class containing a list of {@code Person} objects to be used in tests. + */ +public class TypicalLoans { + + public static final Loan ACTIVE_NON_OVERDUE_LOAN = new LoanBuilder().build(); + public static final Loan ACTIVE_OVERDUE_LOAN = new LoanBuilder().withId(9998) + .withValue(new BigDecimal("1000.00")).withStartDate("2020-06-01").withReturnDate("2021-01-01") + .withAssignee(BOB).build(); + public static final Loan INACTIVE_LOAN = new LoanBuilder().withId(9997) + .withValue(new BigDecimal("1123.00")).withStartDate("2020-06-01").withReturnDate("2040-12-01") + .withAssignee(CARL).withIsReturned(true).build(); + + private TypicalLoans() {} // prevents instantiation + + public static List getTypicalLoans() { + return new ArrayList<>(Arrays.asList(ACTIVE_NON_OVERDUE_LOAN, ACTIVE_OVERDUE_LOAN, INACTIVE_LOAN)); + } + +} diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java index fec76fb7129..b3cbd9d2b09 100644 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ b/src/test/java/seedu/address/testutil/TypicalPersons.java @@ -22,7 +22,6 @@ * A utility class containing a list of {@code Person} objects to be used in tests. */ public class TypicalPersons { - public static final Person ALICE = new PersonBuilder().withName("Alice Pauline") .withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com") .withPhone("94351253") diff --git a/src/test/java/seedu/address/testutil/TypicalPersonsWithLoans.java b/src/test/java/seedu/address/testutil/TypicalPersonsWithLoans.java new file mode 100644 index 00000000000..d49ced3bd9c --- /dev/null +++ b/src/test/java/seedu/address/testutil/TypicalPersonsWithLoans.java @@ -0,0 +1,98 @@ +package seedu.address.testutil; + +import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.DateUtil; +import seedu.address.model.AddressBook; +import seedu.address.model.person.Person; +import seedu.address.model.person.UniqueLoanList; + +/** + * A utility class containing a list of {@code Person} objects to be used in tests. + */ +public class TypicalPersonsWithLoans { + public static final Person ALICE = new PersonBuilder().withName("Alice Pauline") + .withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com") + .withPhone("94351253") + .withTags("friends").build(); + public static final Person BENSON = new PersonBuilder().withName("Benson Meier") + .withAddress("311, Clementi Ave 2, #02-25") + .withEmail("johnd@example.com").withPhone("98765432") + .withTags("owesMoney", "friends").build(); + public static final Person CARL = new PersonBuilder().withName("Carl Kurz").withPhone("95352563") + .withEmail("heinz@example.com").withAddress("wall street").build(); + public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533") + .withEmail("cornelia@example.com").withAddress("10th street").withTags("friends").build(); + public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("9482224") + .withEmail("werner@example.com").withAddress("michegan ave").build(); + public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427") + .withEmail("lydia@example.com").withAddress("little tokyo").build(); + public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("9482442") + .withEmail("anna@example.com").withAddress("4th street").build(); + + // Manually added + public static final Person HOON = new PersonBuilder().withName("Hoon Meier").withPhone("8482424") + .withEmail("stefan@example.com").withAddress("little india").build(); + public static final Person IDA = new PersonBuilder().withName("Ida Mueller").withPhone("8482131") + .withEmail("hans@example.com").withAddress("chicago ave").build(); + + // Manually added - Person's details found in {@code CommandTestUtil} + public static final Person AMY = new PersonBuilder().withName(VALID_NAME_AMY).withPhone(VALID_PHONE_AMY) + .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).withTags(VALID_TAG_FRIEND).build(); + public static final Person BOB = new PersonBuilder().withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) + .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND) + .build(); + + public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER + + private TypicalPersonsWithLoans() {} // prevents instantiation + + /** + * Returns an {@code LoanRecords} stub with some typical loans. + */ + public static UniqueLoanList loanRecords() { + UniqueLoanList uniqueLoanList = new UniqueLoanList(); + try { + uniqueLoanList.addLoan(BigDecimal.valueOf(100), DateUtil.parse("2020-01-01"), + DateUtil.parse("2020-01-13"), ALICE); + uniqueLoanList.addLoan(BigDecimal.valueOf(200), DateUtil.parse("2020-02-01"), + DateUtil.parse("2020-02-13"), BENSON); + uniqueLoanList.addLoan(BigDecimal.valueOf(300), DateUtil.parse("2020-02-13"), + DateUtil.parse("2020-02-14"), CARL); + } catch (IllegalValueException e) { + e.printStackTrace(); + } + return uniqueLoanList; + } + + /** + * Returns an {@code AddressBook} with all the typical persons. + */ + public static AddressBook getTypicalAddressBook() { + AddressBook ab = new AddressBook(); + for (Person person : getTypicalPersons()) { + ab.addPerson(person); + } + ab.setLoans(loanRecords().getLoanList()); + return ab; + } + + public static List getTypicalPersons() { + return new ArrayList<>(Arrays.asList(ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE)); + } +}