diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000000..72408a5f0d8 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,5 @@ +ignore: + - "src/main/java/spleetwaise/commons/ui/*" + - "src/main/java/spleetwaise/address/ui/*" + - "src/main/java/spleetwaise/transaction/ui/*" + - "src/main/java/spleetwaise/commons/MainApp.java" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..769ca64d037 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @global-owner1 and @global-owner2 will be requested for +# review when someone opens a pull request. +* @seeyangzhi @seanlim @rollingpencil @gavinsin @Dino-Nuggies diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 3f454a0be30..eeb7149483c 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,12 +1,12 @@ name: Java CI -on: [push, pull_request] +on: [ push, pull_request, workflow_dispatch ] jobs: build: strategy: matrix: - platform: [ubuntu-latest, macos-latest, windows-latest] + platform: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: @@ -23,7 +23,7 @@ jobs: - name: Run repository-wide tests if: runner.os == 'Linux' - working-directory: ${{ github.workspace }}/.github + working-directory: ${{ github.workspace }}/.github run: ./run-checks.sh - name: Validate Gradle Wrapper diff --git a/README.md b/README.md index 16208adb9b6..b1fa66fcd19 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,34 @@ -[![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/AY2425S1-CS2103-F13-1/tp/actions/workflows/gradle.yml/badge.svg?branch=master)](https://github.com/AY2425S1-CS2103-F13-1/tp/actions?query=branch%3Amaster) + +[![codecov](https://codecov.io/gh/AY2425S1-CS2103-F13-1/tp/graph/badge.svg?token=91MOH0UZHU)](https://codecov.io/gh/AY2425S1-CS2103-F13-1/tp) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org/#contributing-to-se-edu) for more info. +--- +# [CS2103-F13-1] SpleetWaise + +> SpleetWaise builds on [AddressBook Level 3 (AB3)](https://se-education.org/addressbook-level3/), **a desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still offering the benefits of a Graphical User Interface (GUI). _Designed to streamline expense tracking for students_, SpleetWaise makes it easy to record and monitor both personal and shared expenses with contacts saved in the address book. With features to keep track of balances with friends, it eliminates the confusion often associated with managing shared costs, providing a clear, organised view of who has owes what. If you can type fast, SpleetWaise lets you handle your contact and expense management tasks more efficiently than traditional GUI apps, offering students a stress-free way to manage their expenses and shared balances with contacts. + +> **Target User**: Tech-savvy students looking to track shared expenses and manage who owes what. + +--- + +* For the detailed documentation of this project, see + the [SpleetWaise Product Website](https://ay2425s1-cs2103-f13-1.github.io/tp/). + +--- + +## Git Workflow + +This project uses the [Feature Branch Flow](https://nus-cs2103-ay2425s1.github.io/website/se-book-adapted/chapters/revisionControl.html#feature-branch-flow) with rules set to protect the `master` branch. PRs are mandatory and at least 1 member requires review. + +> Feature branches are branches being used to develop an individual story. They should be named "{issue-number}-{summary}" where "{issue-number}" is the issue number corresponding to the feature you are implementing and "{summary}" is a brief summary of the feature `formatted-like-this`. + +## Acknowledgement + +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). + +- Libraries + used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5), [Mockito](https://github.com/mockito/mockito), [TestFX](https://github.com/TestFX/TestFX) +- References + used: [SE-EDU initiative](https://se-education.org/), [AB4](https://github.com/se-edu/addressbook-level4) diff --git a/build.gradle b/build.gradle index 0db3743584e..f9633a9b62b 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id 'jacoco' } -mainClassName = 'seedu.address.Main' +mainClassName = 'spleetwaise.commons.Main' sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -20,20 +20,44 @@ checkstyle { toolVersion = '10.2' } +tasks.register('guiTests', Test) { + useJUnitPlatform() + include 'systemtests/**' + enableAssertions = true +} + +tasks.register('nonGuiTests', Test) { + useJUnitPlatform() + include 'spleetwaise/address/**' + include 'spleetwaise/common/**' + include 'spleetwaise/transaction/**' + enableAssertions = true +} + +tasks.register('allTests') { + dependsOn nonGuiTests, guiTests +} + test { useJUnitPlatform() finalizedBy jacocoTestReport + + systemProperty 'testfx.setup.timeout', '60000' + systemProperty 'java.awt.headless', 'true' + systemProperty 'prism.order', 'sw' + systemProperty 'prism.text', 't2k' + enableAssertions = true + + exclude 'systemtests/**' } -task coverage(type: JacocoReport) { +tasks.register('coverage', JacocoReport) { sourceDirectories.from files(sourceSets.main.allSource.srcDirs) classDirectories.from files(sourceSets.main.output) executionData.from files(jacocoTestReport.executionData) - afterEvaluate { - classDirectories.from files(classDirectories.files.collect { - fileTree(dir: it, exclude: ['**/*.jar']) - }) - } + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: ['**/*.jar']) + })) reports { html.required = true xml.required = true @@ -63,10 +87,24 @@ dependencies { testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion + + testImplementation "org.testfx:testfx-core:4.0.18" + testImplementation "org.testfx:testfx-junit5:4.0.18" + testImplementation group: 'org.hamcrest', name: 'hamcrest', version: '2.1' + testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.13.2' + + testImplementation "org.mockito:mockito-core:5.14.2" + testImplementation 'org.mockito:mockito-junit-jupiter:5.14.2' // Add Mockito JUnit 5 integration } shadowJar { - archiveFileName = 'addressbook.jar' + archiveBaseName = "spleetwaise" + archiveVersion = "1.5.1" + archiveClassifier = null +} + +run { + enableAssertions = true } -defaultTasks 'clean', 'test' +defaultTasks 'clean', 'nonGuiTests', 'coverage' diff --git a/config/Eclipse.xml b/config/Eclipse.xml new file mode 100644 index 00000000000..2fb572f5beb --- /dev/null +++ b/config/Eclipse.xml @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/IntelliJ.xml b/config/IntelliJ.xml new file mode 100644 index 00000000000..6cd3225c170 --- /dev/null +++ b/config/IntelliJ.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + diff --git a/docs/AboutUs.md b/docs/AboutUs.md index ff3f04abd02..da23bcab9ab 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -3,57 +3,79 @@ layout: page title: About Us --- -We are a team based in the [School of Computing, National University of Singapore](https://www.comp.nus.edu.sg). +We are a team based in +the [School of Computing, National University of Singapore](https://www.comp.nus.edu.sg). You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Goh Yong Jing - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[email](e0693145@u.nus.edu)] +[[LinkedIn](https://www.linkedin.com/in/yong-jing-goh-948605219/)] +[[Github](https://github.com/gohyongjing)] -* Role: Project Advisor +- Role: Project Advisor -### Jane Doe +### See Yang Zhi - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[homepage](https://yangzhi.dev)] +[[github](https://github.com/SeeYangZhi)] +[[portfolio](team/seeyangzhi.md)] -* Role: Team Lead -* Responsibilities: UI +- Role: Team Lead +- Responsibilities: + - Scheduling and tracking + - In charge of defining, assigning, and tracking project tasks. + - Deliverables and deadlines + - Ensure project deliverables are done on time and in the right format. + - Documentation + - Responsible for the quality of various project documents. -### Johnny Doe +### Sean Lim - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/seanlim)] [[bsky](https://seanlkx.bsky.social/)] -* Role: Developer -* Responsibilities: Data +- Role: In charge of `Model`, Code Quality +- Responsibilities: + - Manage the models we will use for SpleetWaise. + - Code Reviews to uphold coding standards. -### Jean Doe +### Dong Qianbo - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/rollingpencil)] -* Role: Developer -* Responsibilities: Dev Ops + Threading +- Role: In charge of `Storage`, Integration +- Responsibilities: + - Ensure data format are consistent. + - In charge of versioning of the code, maintaining the code repository, and + integrating various parts of the software to create a whole. -### James Doe +### He Yiheng - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/Dino-Nuggies)] -* Role: Developer -* Responsibilities: UI +- Current role: In charge of `Logic` +- Responsibilities: + - Act as the gatekeeper of quality for `Logic` component + +### Gavin Sin + + + +[[github](http://github.com/gavinsin)] + +- Role: In charge of `UI`, Testing +- Responsibilities: Manage the UI for SpleetWaise to ensure consistency. Code + Testing to ensure bug-free application. diff --git a/docs/DevOps.md b/docs/DevOps.md index d2fd91a6001..c6a45718444 100644 --- a/docs/DevOps.md +++ b/docs/DevOps.md @@ -8,18 +8,43 @@ title: DevOps guide -------------------------------------------------------------------------------------------------------------------- -## Build automation +## Code Formatting + +This project uses the [Java coding standard (basic) +](https://se-education.org/guides/conventions/java/basic.html) and the Google Java style guide. The code style is +enforced using Checkstyle. The checks are run as part of the CI process. + +Developers are recommended to import the [`Intellij IDEA code style XML`](/config/IntelliJ.xml) and [ +`Eclipse XML Profile`](/config/Eclipse.xml) from the `config` folder to IntelliJ IDEA to ensure that the code style is +consistent across all developers after reformatting. Follow the following steps to import the code style settings: + +1. Navigate to `Settings/Preferences` -> `Editor` -> `Code Style` -> `Java`. +2. Beside scheme, click on the gear icon and select `Import Scheme` -> `Intellij IDEA code style XML`. +3. Select the `IntelliJ.xml` file from the `config` folder. +4. Click `OK` to apply the code style. +5. Beside scheme, click on the gear icon and select `Import Scheme` -> `Eclipse XML Profile`. +6. Select the `Eclipse.xml` file from the `config` folder. +7. Check `Current Scheme` and click `OK` to apply the code style. +8. To reformat the code, press `Ctrl + Alt + L` on Windows or `Shift + Cmd + Option + L` on macOS – Ensure the option of + `Optimise Imports` is selected. +9. If you are prompted with `Settings may be overridden by EditorConfig` in `Settings/Preferences` -> `Editor` -> + `Code Style`, click `disable`. + +\* If you are using a different IDE, you can refer to the respective IDE's documentation on how to import code style +settings. But we recommend using IntelliJ IDEA for this project. -This project uses Gradle for **build automation and dependency management**. **You are recommended to read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html)**. +## Build automation +This project uses Gradle for **build automation and dependency management**. **You are recommended to +read [this Gradle Tutorial from the se-edu/guides](https://se-education.org/guides/tutorials/gradle.html)**. Given below are how to use Gradle for some important project tasks. - * **`clean`**: Deletes the files created during the previous build tasks (e.g. files in the `build` folder).
e.g. `./gradlew clean` -* **`shadowJar`**: Uses the ShadowJar plugin to creat a fat JAR file in the `build/lib` folder, *if the current file is outdated*.
+* **`shadowJar`**: Uses the ShadowJar plugin to creat a fat JAR file in the `build/lib` folder, *if the current file is + outdated*.
e.g. `./gradlew shadowJar`. * **`run`**: Builds and runs the application.
@@ -29,28 +54,39 @@ Given below are how to use Gradle for some important project tasks. **`checkstyleTest`**: Runs the code style check for the test code base. * **`test`**: Runs all tests. - * `./gradlew test` — Runs all tests - * `./gradlew clean test` — Cleans the project and runs tests + * `./gradlew test`— Runs all tests + * `./gradlew clean test`— Cleans the project and runs tests -------------------------------------------------------------------------------------------------------------------- ## Continuous integration (CI) -This project uses GitHub Actions for CI. The project comes with the necessary GitHub Actions configurations files (in the `.github/workflows` folder). No further setting up required. +This project uses GitHub Actions for CI. The project comes with the necessary GitHub Actions configurations files (in +the `.github/workflows` folder). No further setting up required. ### Code coverage -As part of CI, this project uses Codecov to generate coverage reports. When CI runs, it will generate code coverage data (based on the tests run by CI) and upload that data to the CodeCov website, which in turn can provide you more info about the coverage of your tests. +As part of CI, this project uses Codecov to generate coverage reports. When CI runs, it will generate code coverage +data (based on the tests run by CI) and upload that data to the CodeCov website, which in turn can provide you more info +about the coverage of your tests. -However, because Codecov is known to run into intermittent problems (e.g., report upload fails) due to issues on the Codecov service side, the CI is configured to pass even if the Codecov task failed. Therefore, developers are advised to check the code coverage levels periodically and take corrective actions if the coverage level falls below desired levels. +However, because Codecov is known to run into intermittent problems (e.g., report upload fails) due to issues on the +Codecov service side, the CI is configured to pass even if the Codecov task failed. Therefore, developers are advised to +check the code coverage levels periodically and take corrective actions if the coverage level falls below desired +levels. -To enable Codecov for forks of this project, follow the steps given in [this se-edu guide](https://se-education.org/guides/tutorials/codecov.html). +To enable Codecov for forks of this project, follow the steps given +in [this se-edu guide](https://se-education.org/guides/tutorials/codecov.html). ### Repository-wide checks -In addition to running Gradle checks, CI includes some repository-wide checks. Unlike the Gradle checks which only cover files used in the build process, these repository-wide checks cover all files in the repository. They check for repository rules which are hard to enforce on development machines such as line ending requirements. +In addition to running Gradle checks, CI includes some repository-wide checks. Unlike the Gradle checks which only cover +files used in the build process, these repository-wide checks cover all files in the repository. They check for +repository rules which are hard to enforce on development machines such as line ending requirements. -These checks are implemented as POSIX shell scripts, and thus can only be run on POSIX-compliant operating systems such as macOS and Linux. To run all checks locally on these operating systems, execute the following in the repository root directory: +These checks are implemented as POSIX shell scripts, and thus can only be run on POSIX-compliant operating systems such +as macOS and Linux. To run all checks locally on these operating systems, execute the following in the repository root +directory: `./config/travis/run-checks.sh` @@ -58,12 +94,14 @@ Any warnings or errors will be printed out to the console. **If adding new checks:** -* Checks are implemented as executable `check-*` scripts within the `.github` directory. The `run-checks.sh` script will automatically pick up and run files named as such. That is, you can add more such files if you need and the CI will do the rest. +* Checks are implemented as executable `check-*` scripts within the `.github` directory. The `run-checks.sh` script will + automatically pick up and run files named as such. That is, you can add more such files if you need and the CI will do + the rest. * Check scripts should print out errors in the format `SEVERITY:FILENAME:LINE: MESSAGE` - * SEVERITY is either ERROR or WARN. - * FILENAME is the path to the file relative to the current directory. - * LINE is the line of the file where the error occurred and MESSAGE is the message explaining the error. + * SEVERITY is either ERROR or WARN. + * FILENAME is the path to the file relative to the current directory. + * LINE is the line of the file where the error occurred and MESSAGE is the message explaining the error. * Check scripts must exit with a non-zero exit code if any errors occur. @@ -73,7 +111,9 @@ Any warnings or errors will be printed out to the console. Here are the steps to create a new release. -1. Update the version number in [`MainApp.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java). -1. Generate a fat JAR file using Gradle (i.e., `gradlew shadowJar`). -1. Tag the repo with the version number. e.g. `v0.1` -1. [Create a new release using GitHub](https://help.github.com/articles/creating-releases/). Upload the JAR file you created. +1. Update the version number in [ + `MainApp.java`](https://github.com/AY2425S1-CS2103-F13-1/tp/blob/master/src/main/java/spleetwaise/commons/MainApp.java). +2. Generate a fat JAR file using Gradle (i.e., `gradlew shadowJar`). +3. Tag the repo with the version number. e.g. `v0.1` +4. [Create a new release using GitHub](https://help.github.com/articles/creating-releases/). Upload the JAR file you + created. diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 743c65a49d2..80484ba63d1 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,154 +2,56 @@ layout: page title: Developer Guide --- -* Table of Contents + +- Table of Contents {:toc} --------------------------------------------------------------------------------------------------------------------- +--- ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +- Libraries + used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5), [Mockito](https://github.com/mockito/mockito), [TestFX](https://github.com/TestFX/TestFX) +- References + used: [SE-EDU initiative](https://se-education.org/), [AB4](https://github.com/se-edu/addressbook-level4) --------------------------------------------------------------------------------------------------------------------- +--- ## **Setting up, getting started** Refer to the guide [_Setting up and getting started_](SettingUp.md). --------------------------------------------------------------------------------------------------------------------- +--- ## **Design**
: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 - - -The ***Architecture Diagram*** given above explains the high-level design of the App. - -Given below is a quick overview of main components and how they interact with each other. - -**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. -* 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. - -The bulk of the app's work is done by the following four components: - -* [**`UI`**](#ui-component): The UI of the App. -* [**`Logic`**](#logic-component): The command executor. -* [**`Model`**](#model-component): Holds the data of the App in memory. -* [**`Storage`**](#storage-component): Reads data from, and writes data to, the hard disk. - -[**`Commons`**](#common-classes) represents a collection of classes used by multiple other 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`. - - - -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. - -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. - - - -The sections below give more details of each component. +{% include DeveloperGuide/Design/architecture.md %} ### 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) - -![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` component, - -* executes user commands using the `Logic` component. -* listens for changes to `Model` data so that the UI can be updated with the modified data. -* keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +{% include DeveloperGuide/Design/ui.md %} ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/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. - -![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) - -
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram. -
- -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. 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. -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: - - - -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. +{% include DeveloperGuide/Design/logic.md %} ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) - - - - -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) - -
: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.
- - - -
+{% include DeveloperGuide/Design/model.md %} ### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/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`) - -### Common classes - -Classes used by multiple components are in the `seedu.address.commons` package. +{% include DeveloperGuide/Design/storage.md %} --------------------------------------------------------------------------------------------------------------------- +--- ## **Implementation** @@ -161,9 +63,9 @@ This section describes some noteworthy details on how certain features are imple The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +- `VersionedAddressBook#commit()` — Saves the current address book state in its history. +- `VersionedAddressBook#undo()` — Restores the previous address book state from its history. +- `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. @@ -189,8 +91,7 @@ Step 4. The user now decides that adding the person was a mistake, and decides t ![UndoRedoState3](images/UndoRedoState3.png) -
: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. +
: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.
@@ -228,14 +129,13 @@ The following activity diagram summarizes what happens when a user executes a ne **Aspect: How undo & redo executes:** -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +- **Alternative 1 (current choice):** Saves the entire address book. + - Pros: Easy to implement. + - Cons: May have performance issues in terms of memory usage. -* **Alternative 2:** 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:** 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. _{more aspects and alternatives to be added}_ @@ -243,118 +143,124 @@ _{more aspects and alternatives to be added}_ _{Explain here how the data archiving feature will be implemented}_ - --------------------------------------------------------------------------------------------------------------------- +--- ## **Documentation, logging, testing, configuration, dev-ops** -* [Documentation guide](Documentation.md) -* [Testing guide](Testing.md) -* [Logging guide](Logging.md) -* [Configuration guide](Configuration.md) -* [DevOps guide](DevOps.md) +- [Documentation guide](Documentation.md) +- [Testing guide](Testing.md) +- [Logging guide](Logging.md) +- [Configuration guide](Configuration.md) +- [DevOps guide](DevOps.md) --------------------------------------------------------------------------------------------------------------------- +--- -## **Appendix: Requirements** +{% include DeveloperGuide/Requirements/index.md %} -### Product scope +--- -**Target user profile**: +## **Appendix: Instructions for manual testing** -* 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 +Given below are instructions to test the app manually. -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +
+:information_source: **Note:** These instructions only provide a starting point for testers to work on; testers are expected to do more *exploratory* testing. +
+### Launch and shutdown -### User stories +1. Initial launch -Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` + 1. Download the jar file and copy into an empty folder -| 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 | + 2. Launch the app
+ Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. -*{More to be added}* +2. Saving window preferences -### Use cases + 1. Resize the window to an optimum size. Move the window to a different location. Close the window. -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) + 2. Re-launch the app
+ Expected: The most recent window size and location is retained. -**Use case: Delete a person** +### Local GUI testing -**MSS** +The available gradle tasks are: guiTests, nonGuiTests, allTests. -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 +- guiTests: all tests under `systemtests` package +- nonGuiTests: all tests under `spleetwaise.address`, `spleetwaise.common`, `spleetwaise.transaction` packages +- allTests: guiTests and nonGuiTests, nonGuiTests will be run before guiTests - Use case ends. +As an example, you can run `gradle nonGuiTests` in the gradle terminal for all tests excluding GUI related tests. +You can navigate the gradle terminal by clicking on elephant icon _(Gradle)_ > terminal icon _(Execute Gradle tasks)_. -**Extensions** +### Adding a person -* 2a. The list is empty. +1. Adding a person while all persons are being shown - Use case ends. + 1. Prerequisites: List all persons using the list command. Multiple persons in the list. -* 3a. The given index is invalid. + 2. Test case: `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01`
+ Expected: Assuming no duplicates, a new contact named John Doe is added to the list. Details of the added contact shown in the status message. - * 3a1. AddressBook shows an error message. + 3. Test case: `add n/ p/ e/ a/`
+ Expected: No person is added. Error details shown in the status message. Status bar remains the same. - Use case resumes at step 2. + 4. Other incorrect add commands to try: `add n/John Doe`, `add p/98765432`, `add e/johnd@example.com`, `add a/John street, block 123, #01-01`
+ Expected: Similar to previous. -*{More to be added}* +### Editing a person -### Non-Functional Requirements +1. Editing a person while all persons are being shown -1. Should work on any _mainstream OS_ as long as it has Java `17` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. -*{More to be added}* + 2. Test case: `edit 1 n/Jane Doe`
+ Expected: Assuming no duplicates, first contact's name is changed to Jane Doe. Details of the edited contact shown in the status message. -### Glossary + 3. Test case: `edit 0 n/Jane Doe`
+ Expected: No person is edited. Error details shown in the status message. -* **Mainstream OS**: Windows, Linux, Unix, MacOS -* **Private contact detail**: A contact detail that is not meant to be shared with others + 4. Other incorrect edit commands to try: `edit`, `edit x n/Jane Doe` (where x is larger than the list size), + `edit 1`
+ Expected: Similar to previous. --------------------------------------------------------------------------------------------------------------------- +### Person Synchronization in transaction list -## **Appendix: Instructions for manual testing** +1. Ensuring person details changed are accurately reflected in all views and models. -Given below are instructions to test the app manually. + 1. Prerequisites: List all persons and transactions using the `list` and `listTxn` command. At least one person and one transaction with that person in the list. -
:information_source: **Note:** These instructions only provide a starting point for testers to work on; -testers are expected to do more *exploratory* testing. + 2. Test case: `edit 1 n/New Name` (where `New Name` is the updated name for the person)
+ Expected: The person's name updates in both the address and transaction lists and the UI (assuming no duplicates). -
+### Adding/deleting a remark for a person -### Launch and shutdown +1. Adding a remark for a person using the `remark` command while all persons are being shown -1. Initial launch + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. - 1. Download the jar file and copy into an empty folder + 2. Test case: `remark 1 r/Likes to swim`
+ Expected: First contact's remark is updated to "Likes to swim". Details of the updated contact shown in the status message. Timestamp in the status bar is updated. + + 3. Test case: `remark 0 r/Likes to swim`
+ Expected: No person's remark is updated. Error details shown in the status message. Status bar remains the same. - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 4. Other incorrect remark commands to try: `remark`, `remark x r/Likes to swim` (where x is larger than the list size)
+ Expected: Similar to previous. + +2. Deleting a remark for a person using the `remark` command while all persons are being shown -1. Saving window preferences + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. + 2. Test case: `remark 1 r/`
+ Expected: First contact's remark is deleted. Details of the contact shown in the status message. - 1. Re-launch the app by double-clicking the jar file.
- Expected: The most recent window size and location is retained. + 3. Test case: `remark 1`
+ Expected: First contact's remark is deleted (shortcut for users intending to empty remark). Details of the contact shown in the status message. -1. _{ more test cases …​ }_ + 4. Test case: `remark 0 r/`
+ Expected: No person's remark is deleted. Error details shown in the status message. ### Deleting a person @@ -362,21 +268,150 @@ testers are expected to do more *exploratory* testing. 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. + 2. Test case: `delete 1`
+ Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. + + 3. Test case: `delete 0`
+ Expected: No person is deleted. Error details shown in the status message. + + 4. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. + +### Adding a transaction + +1. Adding a transaction while all transactions are being shown with minimally 1 person in address book + + 1. Prerequisites: List all persons and transactions using the `list` and `listTxn` command. At least one person in the address book list and multiple transactions in the list + + 2. Test cases: `addTxn 1 amt/12.3 desc/John owes me for dinner`
+ Expected: Assuming no duplicates, a new transaction related to the person in the first index of the address book is added to the list along with description of it. The amount reflected in the transaction is displayed green in the transaction panel to signify that _the user_ is owed. The date of the transaction displays the current day. No categories to be displayed. Details of the added transaction is shown in the status message. + + 3. Test cases: `addTxn 1 amt/-12.3 desc/I owe John for dinner date/10102024`
+ Expected: Assuming no duplicates, a new transaction related to the person in the first index of the address book is added to the list along with description of it. The amount reflected in the transaction is displayed red in the transaction panel to signify that _the user_ owes. The date of the transaction displays `10 Oct 2024`. No categories to be displayed. Details of the added transaction is shown in the status message. + + 4. Test cases: `addTxn 1 amt/12.3 desc/John owes me for dinner cat/FOOD`
+ Expected: Assuming no duplicates, a new transaction related to the person in the first index of the address book is added to the list along with description of it. The amount reflected in the transaction is displayed green in the transaction panel to signify that you are owed. The date of the transaction displays the current day. Category of `FOOD` is displayed. Details of the added transaction is shown in the status message. + + 5. Test cases: `addTxn 1 amt/-12.3 desc/I owe John for dinner date/10102024` when identical transaction exists
+ Expected: No transaction is added. Error details shown in the status message with `Transaction already exists in the transaction book`. Status bar remains the same. + + 6. Test cases: `addTxn 0 amt/ desc/ date/ cat/`
+ Expected: No transaction is added. Error details shown in the status message. Status bar remains the same. + + 7. Other incorrect `addTxn` commands to try: `addTxn 1`, `addTxn amt/1.234`, `addTxn desc/dinner`, `addTxn date/10102024`, `addTxn cat/FOOD`
+ Expected: Similar to previous. + +### Marking a transaction as done + +1. Marking a transaction as done while all transactions are being shown. + + 1. Prerequisites: List all transactions using the `listTxn` command. One transaction is in the list. + + 2. Test cases: `markDone 1`
+ Expected: The first transaction is marked as done. A "done" icon appears next to the person's name for that transaction. Details of the updated transaction shown in the status message. + + 3. Test cases: `markDone 1` (Assumes transaction 1 is already marked)
+ Expected: No change in transaction status. The "done" icon remains. A status message confirms that the transaction is already marked. + + 4. Test cases: `markDone 0`
+ Expected: No transaction is marked. Error details shown in the status message. + + 5. Other incorrect `markDone` commands to try: `markDone`, `markDone x` (where x is larger than the list size)
+ Expected: Similar to previous. + +### Reverting a done transaction back to undone + +1. Reverting a done transaction back to undone while all transactions are being shown. + + 1. Prerequisites: List all transactions using the `listTxn` command. One transaction is in the list. + + 2. Test cases: `markUndone 1`
+ Expected: The first transaction is reverted to undone. The existing "done" icon disappears for that transaction. Details of the updated transaction shown in the status message. + + 3. Test cases: `markUndone 1` (Assumes transaction 1 is already undone)
+ Expected: No change in transaction status. The transaction remains to have no "done" icon. A status message confirms that the transaction is already undone. + + 4. Test cases: `markUndone 0`
+ Expected: No transaction is marked. Error details shown in the status message. + + 5. Other incorrect `markUndone` commands to try: `markUndone`, `markUndone x` (where x is larger than the list size)
+ Expected: Similar to previous. + +### Editing a transaction + +1. Editing a transaction while all persons are being shown + + 1. Prerequisites: List all persons and transactions using the `list` and `listTxn` command respectively. Multiple + persons in the list on the left pane. Multiple transactions in the list on the right pane. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + 2. Test case: `editTxn 1 amt/1.23`
+ Expected: Assuming no duplicates, first contact's amount is changed to $1.23. Details of the edited transaction shown in the status message. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ 3. Test case: `editTxn 0 desc/Updated description`
+ Expected: No transaction is edited. Error details shown in the status message. + + 4. Other incorrect edit commands to try: `editTxn`, `edit x amt/1.23` (where x is larger than the list size), `editTxn 1`
Expected: Similar to previous. -1. _{ more test cases …​ }_ +### Filter Reuse in Transaction List + +1. Maintaining the current filter state when transactions are modified. + + 1. Prerequisites: List all transactions using the `listTxn` command. Apply a filter via `filterTxn` command to the list (e.g., filtering by description containing "mac"). + + 2. Test cases: `addTxn 1 amt/12.3 desc/John owes me for dinner`
+ Expected: The new transaction appears in the filtered list while preserving the existing filter. Details of the new transaction shown in the status message. + + 3. Test cases: `editTxn 1 d/happy meal at mac` (Assumes transaction 1 description is "KFC")
+ Expected: The updated transaction appears in the filtered list while preserving the existing filter. Details of the updated transaction shown in the status message. + + 4. Test cases: `editTxn 1 d/KFC` (Assumes transaction 1 description is "fries at mac")
+ Expected: The updated transaction disappears in the filtered list while preserving the existing filter. Details of the updated transaction shown in the status message. + + 5. Test cases: `markDone 1`, `markUndone 1` (Assumes transaction 1 description is "fries at mac")
+ Expected: The transaction done icon updated in the filtered list while preserving the existing filter. Details of the updated transaction shown in the status message. + +### Filtering the transaction list. + +1. Filtering the transaction list while all persons are being shown + + 1. Prerequisites: List all persons and transactions using the `list` and `listTxn` command respectively. Multiple + persons in the list on the left pane. Multiple transactions in the list on the right pane. + + 2. Test case: `filterTxn 1`
+ Expected: Transaction list will be filtered by the person corresponding to the displayed index 1 in the person + list. + + 3. Test case: `filterTxn 1 amt/1.23`
+ Expected: Transaction list will show transactions related to the person corresponding to the displayed index 1 + in the person list with amount $1.23. + + 4. Test case: `filterTxn 0`
+ Expected: Current displayed transaction list will remain the same. Error details shown in the status message. + + 5. Other incorrect edit commands to try: `filterTxn desc/`, `filterTxn x` (where x is larger than the list size), + `filterTxn amt/1.222`
+ Expected: Similar to previous. + +### Default Behavior on App Startup + +1. Verifying filter state upon app initialization. + + 1. Prerequisites: At least one done transaction and one undone transaction in the list. + + 2. Test cases: Initial Filter on App Startup
+ Expected: The list displays all transactions (both done and undone) by default when the app starts. ### Saving data 1. Dealing with missing/corrupted data files - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. Prerequisites: Multiple persons in `addressbook.json` and multiple transactions in `transactionbook.json` data files.
+ + 2. Test cases: Missing `name` field in `addressbook.json` data file
+ Simulation: Remove the `name` field from a person entry in the JSON file, then start the app.
+ Expected: The person is discarded and not loaded to the app, with log message `WARNING: Address book is possibly corrupted: Person's Name field is missing! Ignoring corrupted person.`. If the corrupted person has any related transactions in the transaction book, they will be discarded accordingly with log message `WARNING: Transaction book is possibly corrupted: Person with id [Person-ID] not found! Ignoring corrupted transactions.` - `[Person-ID]` will be the respective person ID of the corrupted person. -1. _{ more test cases …​ }_ + 3. Test cases: Missing `isDone` field in `transactionbook.json` data file
+ Simulation: Remove the `isDone` field from a transaction entry in the JSON file, then start the app.
+ Expected: The transaction loads as undone by default. Upon closing the app, the transaction is saved as undone in the JSON file. diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index bac5eb36d35..73be7b0a58a 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,48 +1,68 @@ GEM remote: https://rubygems.org/ specs: - activesupport (7.0.7.2) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (7.2.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) - addressable (2.8.4) - public_suffix (>= 2.0.2, < 6.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + base64 (0.2.0) + bigdecimal (3.1.8) coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.11.1) + coffee-script-source (1.12.2) colorator (1.1.0) commonmarker (0.23.10) - concurrent-ruby (1.2.2) - dnsruby (1.70.0) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) + csv (3.3.0) + dnsruby (1.72.2) simpleidn (~> 0.2.1) + drb (2.2.1) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) ethon (0.16.0) ffi (>= 1.15.0) eventmachine (1.2.7) - eventmachine (1.2.7-x64-mingw32) - execjs (2.8.1) - faraday (2.7.5) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.15.5) - ffi (1.15.5-x64-mingw32) + execjs (2.10.0) + faraday (2.12.0) + faraday-net_http (>= 2.0, < 3.4) + json + logger + faraday-net_http (3.3.0) + net-http + ffi (1.17.0-aarch64-linux-gnu) + ffi (1.17.0-aarch64-linux-musl) + ffi (1.17.0-arm-linux-gnu) + ffi (1.17.0-arm-linux-musl) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86-linux-gnu) + ffi (1.17.0-x86-linux-musl) + ffi (1.17.0-x86_64-darwin) + ffi (1.17.0-x86_64-linux-gnu) + ffi (1.17.0-x86_64-linux-musl) forwardable-extended (2.6.0) - gemoji (3.0.1) - github-pages (228) - github-pages-health-check (= 1.17.9) - jekyll (= 3.9.3) - jekyll-avatar (= 0.7.0) - jekyll-coffeescript (= 1.1.1) - jekyll-commonmark-ghpages (= 0.4.0) - jekyll-default-layout (= 0.1.4) - jekyll-feed (= 0.15.1) + gemoji (4.1.0) + github-pages (232) + github-pages-health-check (= 1.18.2) + jekyll (= 3.10.0) + jekyll-avatar (= 0.8.0) + jekyll-coffeescript (= 1.2.2) + jekyll-commonmark-ghpages (= 0.5.1) + jekyll-default-layout (= 0.1.5) + jekyll-feed (= 0.17.0) jekyll-gist (= 1.5.0) - jekyll-github-metadata (= 2.13.0) + jekyll-github-metadata (= 2.16.1) jekyll-include-cache (= 0.2.1) jekyll-mentions (= 1.6.0) jekyll-optional-front-matter (= 0.3.2) @@ -69,30 +89,32 @@ GEM jekyll-theme-tactile (= 0.2.0) jekyll-theme-time-machine (= 0.2.0) jekyll-titles-from-headings (= 0.5.3) - jemoji (= 0.12.0) - kramdown (= 2.3.2) + jemoji (= 0.13.0) + kramdown (= 2.4.0) kramdown-parser-gfm (= 1.1.0) liquid (= 4.0.4) mercenary (~> 0.3) minima (= 2.5.1) - nokogiri (>= 1.13.6, < 2.0) - rouge (= 3.26.0) + nokogiri (>= 1.16.2, < 2.0) + rouge (= 3.30.0) terminal-table (~> 1.4) - github-pages-health-check (1.17.9) + webrick (~> 1.8) + github-pages-health-check (1.18.2) addressable (~> 2.3) dnsruby (~> 1.60) - octokit (~> 4.0) - public_suffix (>= 3.0, < 5.0) + octokit (>= 4, < 8) + public_suffix (>= 3.0, < 6.0) typhoeus (~> 1.3) html-pipeline (2.14.3) activesupport (>= 2) nokogiri (>= 1.4) http_parser.rb (0.8.0) - i18n (1.14.1) + i18n (1.14.6) concurrent-ruby (~> 1.0) - jekyll (3.9.3) + jekyll (3.10.0) addressable (~> 2.4) colorator (~> 1.0) + csv (~> 3.0) em-websocket (~> 0.5) i18n (>= 0.7, < 2) jekyll-sass-converter (~> 1.0) @@ -103,27 +125,28 @@ GEM pathutil (~> 0.9) rouge (>= 1.7, < 4) safe_yaml (~> 1.0) - jekyll-avatar (0.7.0) + webrick (>= 1.0) + jekyll-avatar (0.8.0) jekyll (>= 3.0, < 5.0) - jekyll-coffeescript (1.1.1) + jekyll-coffeescript (1.2.2) coffee-script (~> 2.2) - coffee-script-source (~> 1.11.1) + coffee-script-source (~> 1.12) jekyll-commonmark (1.4.0) commonmarker (~> 0.22) - jekyll-commonmark-ghpages (0.4.0) - commonmarker (~> 0.23.7) - jekyll (~> 3.9.0) + jekyll-commonmark-ghpages (0.5.1) + commonmarker (>= 0.23.7, < 1.1.0) + jekyll (>= 3.9, < 4.0) jekyll-commonmark (~> 1.4.0) rouge (>= 2.0, < 5.0) - jekyll-default-layout (0.1.4) - jekyll (~> 3.0) - jekyll-feed (0.15.1) + jekyll-default-layout (0.1.5) + jekyll (>= 3.0, < 5.0) + jekyll-feed (0.17.0) jekyll (>= 3.7, < 5.0) jekyll-gist (1.5.0) octokit (~> 4.2) - jekyll-github-metadata (2.13.0) + jekyll-github-metadata (2.16.1) jekyll (>= 3.4, < 5.0) - octokit (~> 4.0, != 4.4.0) + octokit (>= 4, < 7, != 4.4.0) jekyll-include-cache (0.2.1) jekyll (>= 3.7, < 5.0) jekyll-mentions (1.6.0) @@ -194,42 +217,52 @@ GEM jekyll (>= 3.3, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) - jemoji (0.12.0) - gemoji (~> 3.0) + jemoji (0.13.0) + gemoji (>= 3, < 5) html-pipeline (~> 2.2) jekyll (>= 3.0, < 5.0) - kramdown (2.3.2) + json (2.7.4) + kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liquid (4.0.4) - listen (3.8.0) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.1) mercenary (0.3.6) - mini_portile2 (2.8.6) minima (2.5.1) jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.19.0) - nokogiri (1.16.5) - mini_portile2 (~> 2.8.2) + minitest (5.25.1) + net-http (0.4.1) + uri + nokogiri (1.16.7-aarch64-linux) + racc (~> 1.4) + nokogiri (1.16.7-arm-linux) + racc (~> 1.4) + nokogiri (1.16.7-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.7-x86-linux) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) octokit (4.25.1) faraday (>= 1, < 3) sawyer (~> 0.9) pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (4.0.7) - racc (1.7.3) + public_suffix (5.1.1) + racc (1.8.1) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) - rexml (3.3.6) - strscan - rouge (3.26.0) - ruby2_keywords (0.0.5) + rexml (3.3.9) + rouge (3.30.0) rubyzip (2.3.2) safe_yaml (1.0.5) sass (3.7.4) @@ -240,25 +273,33 @@ GEM sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - simpleidn (0.2.1) - unf (~> 0.1.4) - strscan (3.1.0) + securerandom (0.3.1) + simpleidn (0.2.3) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unf_ext (0.0.8.2-x64-mingw32) unicode-display_width (1.8.0) - webrick (1.8.1) + uri (0.13.1) + webrick (1.8.2) PLATFORMS - ruby - x64-mingw32 + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86-linux + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES github-pages @@ -266,4 +307,4 @@ DEPENDENCIES webrick BUNDLED WITH - 2.1.4 + 2.5.22 diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 9f832a19674..e3677508b8d 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -6,7 +6,6 @@ title: Setting up and getting started * Table of Contents {:toc} - -------------------------------------------------------------------------------------------------------------------- ## Setting up the project in your computer @@ -19,12 +18,16 @@ Follow the steps in the following guide precisely. Things will not work out if y First, **fork** this repo, and **clone** the fork into your computer. If you plan to use Intellij IDEA (highly recommended): -1. **Configure the JDK**: Follow the guide [_[se-edu/guides] IDEA: Configuring the JDK_](https://se-education.org/guides/tutorials/intellijJdk.html) to ensure Intellij is configured to use **JDK 17**. -1. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into IDEA.
- :exclamation: Note: Importing a Gradle project is slightly different from importing a normal Java project. -1. **Verify the setup**: - 1. Run the `seedu.address.Main` and try a few commands. - 1. [Run the tests](Testing.md) to ensure they all pass. + +1. **Configure the JDK**: Follow the guide [_[se-edu/guides] IDEA: Configuring the + JDK_](https://se-education.org/guides/tutorials/intellijJdk.html) to ensure Intellij is configured to use **JDK 17**. +2. **Import the project as a Gradle project**: Follow the guide [_[se-edu/guides] IDEA: Importing a Gradle + project_](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) to import the project into + IDEA.
+ :exclamation: Note: Importing a Gradle project is slightly different from importing a normal Java project. +3. **Verify the setup**: + 1. Run the `spleetwaise.commons.Main` and try a few commands. + 2. [Run the tests](Testing.md) to ensure they all pass. -------------------------------------------------------------------------------------------------------------------- @@ -32,24 +35,40 @@ If you plan to use Intellij IDEA (highly recommended): 1. **Configure the coding style** - If using IDEA, follow the guide [_[se-edu/guides] IDEA: Configuring the code style_](https://se-education.org/guides/tutorials/intellijCodeStyle.html) to set up IDEA's coding style to match ours. + If using IDEA, follow the guide [_[se-edu/guides] IDEA: Configuring the code + style_](https://se-education.org/guides/tutorials/intellijCodeStyle.html) to set up IDEA's coding style to match + ours.
:bulb: **Tip:** - Optionally, you can follow the guide [_[se-edu/guides] Using Checkstyle_](https://se-education.org/guides/tutorials/checkstyle.html) to find how to use the CheckStyle within IDEA e.g., to report problems _as_ you write code. + Optionally, you can follow the guide [_[se-edu/guides] Using + Checkstyle_](https://se-education.org/guides/tutorials/checkstyle.html) to find how to use the CheckStyle within IDEA + e.g., to report problems _as_ you write code.
-1. **Set up CI** +2. **Git Workflow** + This project uses + the [Feature Branch Flow](https://nus-cs2103-ay2425s1.github.io/website/se-book-adapted/chapters/revisionControl.html#feature-branch-flow) + with rules set to protect the `master` branch. PRs are mandatory and at least 1 member requires review. + + > Feature branches are branches being used to develop an individual story. They should be named " + > {issue-number}-{summary}" where "{issue-number}" is the issue number corresponding to the feature you are + > implementing and "{summary}" is a brief summary of the feature `formatted-like-this`. + +3. **Set up CI** - This project comes with a GitHub Actions config files (in `.github/workflows` folder). When GitHub detects those files, it will run the CI for your project automatically at each push to the `master` branch or to any PR. No set up required. + This project comes with a GitHub Actions config files (in `.github/workflows` folder). When GitHub detects those + files, it will run the CI for your project automatically at each push to the `master` branch or to any PR. No set up + required. -1. **Learn the design** +4. **Learn the design** - When you are ready to start coding, we recommend that you get some sense of the overall design by reading about [AddressBook’s architecture](DeveloperGuide.md#architecture). + When you are ready to start coding, we recommend that you get some sense of the overall design by reading + about [Spleetwaise’s architecture](DeveloperGuide.md#architecture). -1. **Do the tutorials** +5. **Do the tutorials** These tutorials will help you get acquainted with the codebase. - * [Tracing code](tutorials/TracingCode.md) - * [Adding a new command](tutorials/AddRemark.md) - * [Removing fields](tutorials/RemovingFields.md) + * [Tracing code](tutorials/TracingCode.md) + * [Adding a new command](tutorials/AddRemark.md) + * [Removing fields](tutorials/RemovingFields.md) diff --git a/docs/Testing.md b/docs/Testing.md index 8a99e82438a..07d5a48e9fe 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -29,8 +29,8 @@ There are two ways to run tests. This project has three types of tests: 1. *Unit tests* targeting the lowest level methods/classes.
- e.g. `seedu.address.commons.StringUtilTest` + e.g. `spleetwaise.commons.util.StringUtilTest` 1. *Integration tests* that are checking the integration of multiple code units (those code units are assumed to be working).
- e.g. `seedu.address.storage.StorageManagerTest` + e.g. `spleetwaise.commons.storage.StorageManagerTest` 1. Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together.
- e.g. `seedu.address.logic.LogicManagerTest` + e.g. `spleetwaise.commons.logic.LogicManagerTest` diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 84b4ddc4e40..874462407eb 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,7 +3,7 @@ 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. +SpleetWaise builds on [AddressBook Level 3 (AB3)](https://se-education.org/addressbook-level3/), **a desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still offering the benefits of a Graphical User Interface (GUI). Designed to streamline expense tracking for students, SpleetWaise makes it easy to record and monitor both personal and shared expenses with contacts saved in the address book. With features to keep track of balances with friends, it eliminates the confusion often associated with managing shared costs, providing a clear, organised view of who has owes what. If you can type fast, SpleetWaise lets you handle your contact and expense management tasks more efficiently than traditional GUI apps, offering students a stress-free way to manage their expenses and shared balances with contacts. * Table of Contents {:toc} @@ -14,28 +14,31 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo 1. Ensure you have Java `17` or above installed in your Computer. -1. Download the latest `.jar` file from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Download the latest `.jar` file from [here](https://github.com/AY2425S1-CS2103-F13-1/tp/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +3. Copy the file to the folder you want to use as the _home folder_ for SpleetWaise. -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.
+4. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar spleetwaise-[version].jar` + command to run the application (*replace `[version]` with the release version you chose, for example, 1.5).
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.
+5. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will + open the help window.
Some example commands you can try: - * `list` : Lists all contacts. + * `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. + * `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. + * `delete 3` : Deletes the 3rd contact shown in the current list. - * `clear` : Deletes all contacts. + * `clear` : Deletes all contacts. - * `exit` : Exits the app. + * `exit` : Exits the app. -1. Refer to the [Features](#features) below for details of each command. +6. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- @@ -57,57 +60,154 @@ 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` -Shows a message explaning how to access the help page. +Shows a message explaining how to access the help page. ![help message](images/helpMessage.png) Format: `help` - ### Adding a person: `add` Adds a person to the address book. Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +* The `NAME` input allows a wide variety of characters but has some restrictions. Below are examples of valid and invalid inputs. + * Valid inputs include those that contain **letters, numbers, spaces, and certain special characters** such as apostrophes (`'`), hyphens (`-`), periods (`.`), slashes (`/`), ampersands (`&`), quotation marks (`"`), and parentheses (`()`). + - `John Doe` (letters and space) + - `Betsy O'Connor` (apostrophe) + - `Jean-Luc` (hyphen) + - `J.P. Morgan` (period) + - `John s/o Tan` (slash) + - `Anne-Marie & Sons` (ampersand and hyphen) + - `John "Johnny" Doe` (quotation marks) + - `Richard (Rick) Roe` (parentheses) + * Invalid inputs include those that contain special characters such as `*`, `@`, `#`, `!`, `^`, `%`, `$`, or any characters from Arabic, or Latin scripts like `Æ` or Chinese characters. + - `peter*` (contains `*`) + - `john@doe` (contains `@`) + - `王小明` (non-Latin characters like Chinese) + - `X Æ A-12` (non-Latin characters like `Æ` or Arabic `عبد العزيز`) + +
:bulb: **Tip:** +A person with an Indian name containing "s/o" denoting "son of" can be added as `add n/John s/o Jason p/98765432 e/johnd@example.com a/John street, block 123, #01-01` +
+
:bulb: **Tip:** +A person with an Indian name containing "d/o" denoting "daughter of" can be added as `add n/Genevieve d/o Jason p/98765432 e/johnd@example.com a/John street, block 123, #01-01` +
:bulb: **Tip:** A person can have any number of tags (including 0)
Examples: + * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` * `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +### Adding a transaction: `addTxn` + +Adds a transaction to the transaction book. + +Format: `addTxn INDEX amt/AMOUNT desc/DESCRIPTION [date/DATE] [status/STATUS] [cat/CATEGORY]...` + +* The `INDEX` refers to the index of the person currently displayed in the address book panel (as we are adding the transaction related to the person). +* The `AMOUNT` accepts a decimal number with up to 2 decimal places. A `-` symbol should be added before the number to indicate negative amount, indicating the transaction is one that the user owes the chosen person at the index. +* The `DESCRIPTION` accepts a string of words with a limit of 120 characters. +* The `DATE` accepts date formatted in the form `DDMMYYYY` i.e.`10102024`. +* The `STATUS` accepts case-sensitive string that is either 'Done' or 'Not Done'. +* The `CATEGORY` accepts non-empty strings that are alphanumeric with spaces. Category will be capitalised automatically. + +:exclamation: **Important:** Two identical Transactions with duplicated fields cannot be added into the transaction +book. The duplicated fields refers to `INDEX`, `AMOUNT`, `DESCRIPTION`, and `DATE` fields where combined it should +form a unique transaction.
+> For consistency and to avoid redundancy, identical transactions with the same details across all fields will not be added to the transaction book. This ensures that each entry remains unique, preventing accidental duplicates and maintaining the clarity of transaction records. If a similar transaction occurs on a different occasion in the same day, we recommend users to tweak the desc field to reflect the specific context.
+> +> For example:
- `addTxn 1 amt/2.50 desc/sean owes me for morning latte`
- `addTxn 1 amt/2.50 desc/sean owes me for afternoon latte` + +:bulb: **Tip:** The index aligns with the address book including when it is filtered.
+:bulb: **Tip:** If the transaction happened on the current day, the date parameter can be omitted.
+:bulb: **Tip:** A person can have any number of categories (including 0)
+:bulb: **Tip:** Positive Amount Transaction indicates someone owes _the user_ an amount.
+:bulb: **Tip:** Negative Amount Transaction indicates _the user_ owes someone an amount.
+:bulb: **Tip:** Categories can be keyed in both lower and upper case without worrying about duplication as it will be capitalised automatically.
+:bulb: **Tip:** By default, a newly created transaction is set as undone - _e.g._ if the transaction is added as `addTxn 1 amt/12.3 desc/John owes me for dinner`, this transaction is not done, John still owes _the user_. + +Examples: + +* `addTxn 1 amt/12.3 desc/John owes me for dinner ` +* `addTxn 1 amt/-24.3 desc/I owe John for dinner date/10102024` +* `addTxn 1 amt/-24.3 desc/I owe John for dinner date/10102024 cat/FOOD` + ### Listing all persons : `list` Shows a list of all persons in the address book. Format: `list` +:bulb: **Tip:** This command can be used to reset the filter applied on the person list caused by a `find` command +operation. + +### Listing all transactions : `listTxn` + +Shows a list of all transactions in the transaction book. + +Format: `listTxn` + +:bulb: **Tip:** This command can be used to reset the filter applied on transaction list caused by a `fitlerTxn` +command operation. + ### 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]…​` +Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [r/REMARK] [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, …​ +* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. + The index **must be a positive integer** 1, 2, 3, …​ * At least one of the optional fields must be provided. * Existing values will be updated to the input values. * When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +* You can remove all the person’s tags by typing `t/` without specifying any tags after it. +* You can remove the person’s remark by typing `r/` without specifying any remark after it. 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 r/` Edits the 2nd person by deleting the remark. +* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. + +### Editing a transaction : `editTxn ` + +Edits an existing transaction in the transaction book. + +Format: `editTxn INDEX [p/PHONE_NUMBER] [amt/AMOUNT] [desc/DESCRIPTION] [date/DATE] [cat/CATEGORY]...` + +* Edits the transaction at the specified `INDEX`. The index refers to the index number shown in the displayed transaction list. + The index **must be a positive integer** 1, 2, 3, … +* At least one of the optional fields must be provided. +* Existing values will be updated to the input values. +* When editing categories, the existing categories of the transaction will be removed i.e adding of categories is not + cumulative. +* You can remove all the person’s categories by typing `cat/` without specifying any categories after it. +* When editing the person related to the transaction through specifying `[p/PHONE_NUMBER]`, the person with the input phone number must be in the address book. + +:bulb: **Tip:** Status can be edited via `markDone` or `markUndone` command. Support for editing status through `editTxn` may be considered by the devs in future releases. + +Examples: + +* `editTxn 1 p/92624417 desc/Hello world` Edits the 1st transaction to be related to the person with phone number `92624417` and edits the description of the 1st transaction to be `Hello world` (ensure that the phone number is valid, i.e. it exists in the address book). +* `editTxn 2 cat/` Edits the 2nd transaction by removing all existing categories. ### Locating persons by name: `find` @@ -123,10 +223,64 @@ Format: `find KEYWORD [MORE_KEYWORDS]` e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` Examples: + * `find John` returns `john` and `John Doe` * `find alex david` returns `Alex Yeoh`, `David Li`
![result for 'find alex david'](images/findAlexDavidResult.png) +### Filtering transactions: `filterTxn` + +Filter transactions with a any combination of the following parameters: +* the specified person identified by their index in the displayed person list +* and/or amount +* and/or description +* and/or date +* and/or status +* and/or positive/negative amount +* and/or category + +Format: `filterTxn [INDEX] [amt/AMOUNT] [desc/DESCRIPTION] [date/DATE] [status/STATUS] [amtsign/AMOUNT_SIGN] [cat/CATEGORY]` + +* The command requires at least one of the above optional prefixes to be provided. +* As more prefixes are provided, the filter becomes more specific. +* The `INDEX` refers to the index number shown in the displayed person list. + The index **must be a positive integer** 1, 2, 3, …​ +* The `AMOUNT` accepts a decimal number with up to 2 decimal places. A `-` symbol should be added before the number to indicate negative amount, indicating the transaction is one that the user owes the chosen person at the index. Results will display transactions with the exact amount if it exists. +* The `DATE` accepts date formatted in the form `DDMMYYYY` i.e.`10102024`. +* The `DESCRIPTION` accepts a string of words. + * The description filter is case-insensitive. e.g `hans` will match `Hans` +* The `STATUS` accepts either `Done` or `Not Done` to indicate filtering for transactions that are done or not done. +* The `AMOUNT_SIGN` accepts either `Pos` or `Neg` to indicate filtering for transactions with amount that are + positive or negative respectively. +* The `CATEGORY` accepts non-empty strings that are alphanumeric with spaces. Category will be capitalised automatically. + +Examples:
+ +* Given the example transaction book:
+ ![Given the example transaction book](images/filterTxnExample.png) +* `filterTxn 1` returns all transactions with the person `Alex Yeoh`. Given that `1` is the index of `Alex Yeoh` in the displayed person list.
+ ![result fpr 'filterTxn 1'](images/filterTxnAlexYeohResult.png) +* `filterTxn 2 amt/5.5` returns all transactions with the person `Bernice Yu` with amount `5.50`. Given that `2` is the index of `Bernice Yu` in the displayed person list.
+ ![result for 'filterTxn 2 amt/5.5'](images/filterTxnBerniceYuAmt55Result.png) + +### Adding/Deleting Remarks for a person : `remark` + +Add/Delete remarks for the specified person from the address book. + +Format: `remark INDEX r/REMARK` + +* Add/delete remarks for 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, …​ + +Examples: + +* `list` followed by `remark 2 r/remark for person 2` adds remarks for the 2nd person in the address book. +* `list` followed by `remark 1 r/` deletes the remarks for the 1st person in the address book. +* `find Betsy` followed by `remark 1 r/remark for betsy` adds remarks for the 1st person in the results of the `find` + command. +* `find Betsy` followed by `remark 1 r/` deletes the remarks for the 1st person in the results of the `find` command. + ### Deleting a person : `delete` Deletes the specified person from the address book. @@ -138,15 +292,76 @@ Format: `delete INDEX` * The index **must be a positive integer** 1, 2, 3, …​ Examples: + * `list` followed by `delete 2` deletes the 2nd person in the address book. * `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. -### Clearing all entries : `clear` +### Deleting a transaction : `deleteTxn` + +Deletes the specified transaction from the transaction book. + +Format: `deleteTxn INDEX` + +* Deletes the transaction at the specified `INDEX`. +* The `INDEX` refers to the index number shown in the displayed transaction list. +* The index **must be a positive integer** 1, 2, 3, …​ + +Examples: + +* `listTxn` followed by `delete 2` deletes the 2nd person in the address book. +* `filterTxn 1` followed by `delete 1` deletes the 1st transaction in the results of the `filterTxn` command. -Clears all entries from the address book. +### Clearing all entries of AddressBook and TransactionBook: `clear` + +Clears all entries from the address book and transaction book. Format: `clear` +### Clearing all entries of TransactionBook : `clearTxn` + +Clears all entries from the transaction book. + +Format: `clearTxn` + +### Marking a transaction as done : `markDone` + +Marks a specified transaction from the transaction book as done. + +Format: `markDone INDEX` + +* Marks a transaction at the specified `INDEX` as done. +* The index refers to the index number shown in the displayed transaction list. +* The index **must be a positive integer** 1, 2, 3, …​ + +
:bulb: **Tip:** +If a transaction is marked as done, a done icon appears for the transaction in GUI. +
+ +Examples: + +* `listTxn` followed by `markDone 2` marks the 2nd transaction in the transaction book as done. +* If a done transaction is marked as done again, the transaction remains done. + +### Unmarking a transaction as done : `markUndone` + +Unmarks a specified transaction from the transaction book as undone. + +Format: `markUndone INDEX` + +* Unmarks a transaction at the specified `INDEX` as undone. +* The index refers to the index number shown in the displayed transaction list. +* The index **must be a positive integer** 1, 2, 3, …​ + +
+:bulb: **Tip:** By default, a new transaction is undone.
+:bulb: **Tip:** If a done transaction is marked as undone, the existing done icon for the transaction in GUI disappears. +
+ +Examples: + +* `listTxn` followed by `markUndone 2` unmarks the 2nd transaction in the transaction book as undone. +* if an undone transaction is marked as undone again, the transaction remains undone. + ### Exiting the program : `exit` Exits the program. @@ -155,15 +370,21 @@ 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. +AddressBook and Transaction 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. +- AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json` +- TransactionBook data are saved automatically as a JSON file `[JAR file location]/data/transactionbook.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. +If changes to the data file make its format invalid, SpleetWaise will discard corrupted data and start as usual. To avoid data loss, it’s recommended to back up the file before making edits. Person and Transactions with invalid fields will be discarded before the application starts.
+Furthermore, certain edits can cause the AddressBook or TransactionBook to behave in unexpected ways (e.g., if a value entered is outside the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly. +Notably, if SpleetWaise encounter a person/transaction with an existing person/transaction ID in the +address/transaction book, it will be discarded. Similarly, if SpleetWaise encounter a transaction with the same +person ID, amount, date and description as an existing transaction in the transaction book, it will be discarded as +well.
### Archiving data files `[coming in v2.0]` @@ -175,25 +396,40 @@ _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 before launching the app, copy the `data` folder over to its home folder from your previous SpleetWaise 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. -2. **If you minimize the Help Window** and then run the `help` command (or use the `Help` menu, or the keyboard shortcut `F1`) again, the original Help Window will remain minimized, and no new Help Window will appear. The remedy is to manually restore the minimized Help Window. +2. **If you minimize the Help Window** and then run the `help` command (or use the `Help` menu, or the keyboard shortcut`F1`) again, the original Help Window will remain minimized, and no new Help Window will appear. The remedy is to manually restore the minimized Help Window. + +-------------------------------------------------------------------------------------------------------------------- + +## Command Summary for Address Book + +| Action | Format, Examples | +|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [r/REMARK] [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 r/James is poor af, do not loan him money 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] [r/REMARK] [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` | +| **Remark** | `remark INDEX r/REMARK` | +| **Help** | `help` | -------------------------------------------------------------------------------------------------------------------- -## 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` +## Command Summary for Transactions + +| Action | Format, Examples | +|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Add** | `addTxn INDEX amt/AMOUNT desc/DESCRIPTION [date/DATE] [status/STATUS] [cat/CATEGORY]`
e.g., `addTxn 1 amt/9999999999.99 desc/Sean owes me a lot for a plot of land in sentosa date/10102024 status/Done cat/LOAN` | +| **Edit** | `editTxn INDEX [p/PHONE_NUMBER] [amt/AMOUNT] [desc/DESCRIPTION] [date/DATE] [cat/CATEGORY]`
e.g., `editTxn 1 p/99999999 amt/9999999999.99 desc/Sean owes me a lot for a plot of land in sentosa date/10102024 cat/LOAN` | +| **List** | `listTxn` | +| **Filter** | `filterTxn [INDEX] [amt/AMOUNT] [desc/DESCRIPTION] [date/DATE] [status/STATUS] [amtsign/AMOUNT_SIGN]`
e.g. `filterTxn 1` | +| **Clear** | `clearTxn` | +| **Mark Done** | `markDone INDEX`
e.g. `markDone 1` | +| **Mark Undone** | `markUndone INDEX`
e.g. `markUndone 1` | diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..1b318ec46eb 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "SpleetWaise" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2425S1-CS2103-F13-1/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_includes/DeveloperGuide/Design/architecture.md b/docs/_includes/DeveloperGuide/Design/architecture.md new file mode 100644 index 00000000000..d81524fcb68 --- /dev/null +++ b/docs/_includes/DeveloperGuide/Design/architecture.md @@ -0,0 +1,58 @@ + + +The **_Architecture Diagram_** given above explains the high-level design of the App. + +> **Disclaimer:** Our team has decided to add the `spleetwaise.commons` package as a common package for classes +> that are used by multiple components, in our case, `address` and `transaction`. This is an enhancement for +> modularity on top of the original design of the AddressBook-Level3 project, which only have a `seedu.address` +> package. The refactoring is almost 90% complete, and we are working on the remaining 10%. + +Given below is a quick overview of main components and how they interact with each other. + +**Main components of the architecture** + +The app consists of three packages: `address`, `transactions`, and `common`. + +- `common` is where the main application logic lives. It contains general classes that are used by multiple components in the app. +- `address` contains classes related to address book. +- `transactions` contains classes related to transactions. + +
:information_source: +**Package structure:** +Packages follow this general package structure:
+- **`logic`**: Contains classes related to commands/command parsing.
+- **`model`**: Contains classes for representing data in the app.
+- **`storage`**: Contains classes related to reading and writing data from, and to storage.
+- **`ui`**: Contains classes related to the GUI of the app.
+
+ +**Main application** + +The entry-point to the app, **`Main`** (consisting of classes [`Main`](https://github.com/AY2425S1-CS2103-F13-1/tp/blob/master/src/main/java/spleetwaise/commons/Main.java) and [`MainApp`](https://github.com/AY2425S1-CS2103-F13-1/tp/blob/master/src/main/java/spleetwaise/commons/MainApp.java)), lives in the `common` package, and is in charge of managing the app's lifecycle. + +- 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. + +The bulk of the app's work is done by the following four components: + +- [**`Ui`**](#ui-component): The UI of the App. +- [**`Logic`**](#logic-component): The command executor. +- [**`Model`**](#model-component): Holds the data of the App in memory. +- [**`Storage`**](#storage-component): Reads data from, and writes data to, the hard disk. + +**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`. + + + +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. + +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. + + + +The sections below give more details of each component. diff --git a/docs/_includes/DeveloperGuide/Design/logic.md b/docs/_includes/DeveloperGuide/Design/logic.md new file mode 100644 index 00000000000..a3605f3313c --- /dev/null +++ b/docs/_includes/DeveloperGuide/Design/logic.md @@ -0,0 +1,41 @@ +**API** : [`Logic.java`](https://github.com/AY2425S1-CS2103-F13-1/tp/blob/master/src/main/java/spleetwaise/commons/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. + +![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) + +
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram. +
+ +#### Dual-parser setup +How the `Logic` component works: + +1. When `Logic` is called upon to execute a command, it will first check whether the command is known by the `AddressBookParser`: + + 1.1. **It is first passed to an `AddressBookParser` object which will attempt to parse it.** If there is a matching command, it creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. + + 1.2. **If there is no matching command, `Logic` will try to parse it with a `TransactionParser`.** If there is a matching command, a similar thing happens as with `AddressBookParser`. The partial sequence diagram below illustrates this case clearly: + + ![Partial sequence diagram for transaction command](images/LogicSequenceDiagram.png) + +2. This ultimately results in a non-null `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. + +3. 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. + +4. 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: + + + +How the parsing works: + +- When called upon to parse a user command, the `AddressBookParser` or `TransactionParser` class creates a respective `AbCommandParser` (`Ab` is a placeholder for some specific address book command name e.g., `AddCommandParser`), which uses the other classes shown above to parse the user command and create a `AbCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. Likewise for `TransactionParser` and `TbCommand`s. + +- All `AbCommandParser` and `TbCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from a common `Parser` interface so that they can be treated similarly where possible e.g, during testing. diff --git a/docs/_includes/DeveloperGuide/Design/model.md b/docs/_includes/DeveloperGuide/Design/model.md new file mode 100644 index 00000000000..9b9f3e0c802 --- /dev/null +++ b/docs/_includes/DeveloperGuide/Design/model.md @@ -0,0 +1,44 @@ +#### `AddressBookModel` + +**API** : [`AddressBookModel.java`](https://github.com/AY2425S1-CS2103-F13-1/tp/blob/master/src/main/java/spleetwaise/address/model/AddressBookModel.java) + + + +The `AddressBookModel` 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. +- 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.
+ + + +
+ +#### `TransactionBookModel` + +**API** : [`TransactionBookModel.java`](https://github.com/AY2425S1-CS2103-F13-1/tp/blob/master/src/main/java/spleetwaise/transaction/model/TransactionBookModel.java) + + + +- stores the transaction book data i.e., all `Transaction` objects (which are contained in a `ObservableList` object). +- stores the currently 'selected' `Transaction` 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. +- 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) + + +#### `CommonModel` + +**API** : [`CommonModel.java`](https://github.com/AY2425S1-CS2103-F13-1/tp/blob/master/src/main/java/spleetwaise/commons/model/CommonModel.java) + + + +The `CommonModel` component: + +- stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. + +#### `CommonModelManager` + +Singleton `CommonModel` class. The singleton instance of this class contains an instance of both `AddressBookModel` and `TransactionBookModel`. The singleton instance acts as a facade, exposing the APIs of both `AddressBookModel` and `TransactionBookModel`. + +Certain transaction-related features need access to data from both the address and transaction book from different areas of the codebase. For this reason, we decided to go with the `CommonModelManager` class design described above. diff --git a/docs/_includes/DeveloperGuide/Design/storage.md b/docs/_includes/DeveloperGuide/Design/storage.md new file mode 100644 index 00000000000..ef133d05de0 --- /dev/null +++ b/docs/_includes/DeveloperGuide/Design/storage.md @@ -0,0 +1,9 @@ +**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) + +Storage Class Diagram + +The `Storage` component, + +* can save address book data, transaction book data and user preference data in JSON format, and read them back into corresponding objects. +* inherits from `AddressBookStorage`, `TransactionBookStorage` 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`) diff --git a/docs/_includes/DeveloperGuide/Design/ui.md b/docs/_includes/DeveloperGuide/Design/ui.md new file mode 100644 index 00000000000..cb6da146759 --- /dev/null +++ b/docs/_includes/DeveloperGuide/Design/ui.md @@ -0,0 +1,13 @@ +The **API** of this component is specified in [`Ui.java`](https://github.com/AY2425S1-CS2103-F13-1/tp/blob/master/src/main/java/spleetwaise/commons/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/AY2425S1-CS2103-F13-1/tp/blob/master/src/main/java/spleetwaise/commons/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2425S1-CS2103-F13-1/tp/blob/master/src/main/resources/view/MainWindow.fxml) + +The `UI` component, + +- executes user commands using the `Logic` component. +- listens for changes to `Model` data so that the UI can be updated with the modified data. +- keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. diff --git a/docs/_includes/DeveloperGuide/Requirements/00_product_scope.md b/docs/_includes/DeveloperGuide/Requirements/00_product_scope.md new file mode 100644 index 00000000000..c9be1dade9f --- /dev/null +++ b/docs/_includes/DeveloperGuide/Requirements/00_product_scope.md @@ -0,0 +1,12 @@ +### Product scope + +**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 +- is a student with a need track the transactions made on a daily basis due to frequent splitting of bills with saved contacts + +**Value proposition**: Simplifies expense tracking for students, making it easy to split and manage shared costs with clarity and peace of mind. diff --git a/docs/_includes/DeveloperGuide/Requirements/01_user_stories.md b/docs/_includes/DeveloperGuide/Requirements/01_user_stories.md new file mode 100644 index 00000000000..5c75491be8d --- /dev/null +++ b/docs/_includes/DeveloperGuide/Requirements/01_user_stories.md @@ -0,0 +1,24 @@ +### 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 | +| `* * *` | user | mark and unmark expenses that are paid off/not paid | track which expenses have been settled | +| `* * *` | user | see how much a specific contact owes | pay them back easily | +| `*` | user living abroad | handle currency conversion | track expenses accurately when paying in different currencies | +| `*` | user who frequently travels | tag expenses by event | manage and track costs associated with specific activities or trips | +| `* * *` | user | view a summary of all expenses and balances | quickly see who owes what and who has overpaid | +| `* *` | student tight on budget | set limits on certain categories | better manage my spendings | +| `* *` | user | access frequently contacted people quickly | record a transaction in a short amount of time | +| `* *` | user | create templates | add transactions quicker | +| `* * *` | user | set up recurring transactions | automate my transactions | +| `* * *` | student | generate a summary report of all expenses | share it with the group and settle payments | +| `* *` | user | detect duplicate expenses | avoid accidentally double-tracking a payment | +| `* *` | student managing multiple categories | tag expenses by category | organize and search for them easily later | diff --git a/docs/_includes/DeveloperGuide/Requirements/02_use_cases.md b/docs/_includes/DeveloperGuide/Requirements/02_use_cases.md new file mode 100644 index 00000000000..ddb423a2c44 --- /dev/null +++ b/docs/_includes/DeveloperGuide/Requirements/02_use_cases.md @@ -0,0 +1,101 @@ +### Use cases + +(For all use cases below, the **System** is `SpleetWaise` and the **Actor** is the `user`, unless specified +otherwise) + +**UC01 - View Usage Instructions** + +Actor: New User + +**MSS** +1. The new user clicks on "Help" at top bar of the application. +2. The system displays usage instructions in a browser. +3. The user reviews the instructions. + +**Extensions** +- **2a.** The user can switch between different sections of the instructions (e.g., FAQs, How-to sections, etc.). + +--- + +**UC02 - Add a New Person** + +**MSS** +1. A user request to add a new person with required details (e.g., name, email, phone number, address). +2. The system validates the input (e.g., checks for valid email format, non-empty fields). +3. The system saves the new person to the address book and displays a success message. +4. The new person is added to the list. + +**Extensions** +- **4a.** The system detects an invalid email format or phone number. + - 4a1. The system shows an error message. + - 4a2. The user have to restart from step 1. + +--- + +**UC03 - Delete a person** + +**MSS** +1. A user requests to list persons +2. The system shows a list of persons +3. The user requests to delete a specific person in the list +4. The system deletes the person + +**Extensions** +- **2a.** The list is empty. + - 2a1. Use case ends. +- **3a.** The given index is invalid. + - 3a1. AddressBook shows an error message. + - 3a2. Use case resumes at step 2. + +--- + +**UC04 - Find a Person by Name** + +**MSS** +1. A user requests to search for a person by first or last name. +2. The system performs a search and displays matched person's details. + +**Extensions** +- **2a.** The system finds no match. + - 2a1. The system displays a message indicating no results. + - 2a2. Use case ends. +- **2b.** The system finds multiple people with identical first or last names. + - 2b1. The system displays the lists of found person along with a message for the user to specify which person. + - 2b2. The user specify which exact person within the list using index. + - 2b3. The system displays the specified person's details. + - 2b4. Use case ends. + +--- + +**UC05 - Mark and Unmark Expenses** + +**MSS** +1. A user requests to list expenses +2. The system shows a list of expenses +3. The user requests to mark or unmark the selected expense. +4. The system updates the status of the expense. +5. The system displays a success message indicating the change. + +--- + +**UC06 - View Person's Owed Amount** + +**MSS** +1. A user requests to list all person. +2. The user requests to filter transaction by a specific person in the list +3. The system displays how much the user owe to the selected person. + +--- + +**UC07 - View Summary of Expenses and Balances** + +**MSS** +1. A user chooses to view a summary of all expenses and balances. +2. The system calculates the total expenses, balances, and individual amounts owed. +3. The system displays the summary to the user in a tabular format. + +**Extensions** +- **3a.** The user requests a filtered summary (e.g., by date or category). + - 3a1. The system filters the summary based on the user’s input. + - 3a2. Use case resumes from step 3. + diff --git a/docs/_includes/DeveloperGuide/Requirements/03_non_functional_requirements.md b/docs/_includes/DeveloperGuide/Requirements/03_non_functional_requirements.md new file mode 100644 index 00000000000..8471bb192c3 --- /dev/null +++ b/docs/_includes/DeveloperGuide/Requirements/03_non_functional_requirements.md @@ -0,0 +1,15 @@ +### Non-Functional Requirements + +1. Should work on any _mainstream OS_ as long as it has Java `17` 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. There should be only one type of currency used in all transactions for MVP, defaulted to SGD. +5. The parameters that related to amount of currency should be dominated in dollars with at most 2 decimal + places for cents, delimited by a decimal point (.). +6. The current format of the JSON format should be compatible with previous version of the software. +7. The product is used only for expense tracking and consolidation, no real world exchange of money will be carried + out. +8. The user should be able to restore and use a backup save file as part of disaster recovery. +9. The user should be able to save the save file on a portable device and continue to work on the save file on + another computer. diff --git a/docs/_includes/DeveloperGuide/Requirements/04_glossary.md b/docs/_includes/DeveloperGuide/Requirements/04_glossary.md new file mode 100644 index 00000000000..842d2ca9ddf --- /dev/null +++ b/docs/_includes/DeveloperGuide/Requirements/04_glossary.md @@ -0,0 +1,11 @@ +### Glossary + +This Spleetwaise app is a _single-user_ application. Transactions and total balances are relative to _the user_. +- **Mainstream OS**: Windows, Linux, Unix, macOS +- **Private contact detail**: A contact detail that is not meant to be shared with others +- **Transaction**: A Transaction represents a record of a financial interaction between _the user_ and another party (another contact). + - **Positive Amount** Transaction: Indicates someone owes _the user_ an amount. + - **Negative Amount** Transaction: Indicates _the user_ owes someone an amount. + - **Undone** Transaction: By default, a newly created transaction is set as undone - _e.g._ if the transaction is added as `addTxn 1 amt/12.3 desc/John owes me for dinner`, this transaction is not done, John still owes _the user_. + - **Done** Transaction: A completed transaction, referring to the previous _e.g._ once John has paid _the user_, he will mark the transaction as done. +- **Total Balance**: Consists of the _the user_'s total income and total expenses over a period, reflected as "You are owed" and "You owe" in the GUI. diff --git a/docs/_includes/DeveloperGuide/Requirements/index.md b/docs/_includes/DeveloperGuide/Requirements/index.md new file mode 100644 index 00000000000..66175058196 --- /dev/null +++ b/docs/_includes/DeveloperGuide/Requirements/index.md @@ -0,0 +1,7 @@ +## **Appendix: Requirements** + +{% include DeveloperGuide/Requirements/00_product_scope.md %} +{% include DeveloperGuide/Requirements/04_glossary.md %} +{% include DeveloperGuide/Requirements/01_user_stories.md %} +{% include DeveloperGuide/Requirements/02_use_cases.md %} +{% include DeveloperGuide/Requirements/03_non_functional_requirements.md %} diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..a4337948f47 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: "SpleetWaise"; font-size: 32px; } } diff --git a/docs/diagrams/ArchitectureDiagram.puml b/docs/diagrams/ArchitectureDiagram.puml index 4c5cf58212e..fc23b3ad491 100644 --- a/docs/diagrams/ArchitectureDiagram.puml +++ b/docs/diagrams/ArchitectureDiagram.puml @@ -4,30 +4,51 @@ !include !include style.puml -Package " "<>{ - Class UI UI_COLOR - Class Logic LOGIC_COLOR - Class Storage STORAGE_COLOR - Class Model MODEL_COLOR - Class Main #grey - Class Commons LOGIC_COLOR_T2 -} +top to bottom direction Class "<$user>" as User MODEL_COLOR_T2 -Class "<$documents>" as File UI_COLOR_T1 +Package "spleetwaise.commons" <> { + Class commons_Main #grey + + together { + Class commons_Logic LOGIC_COLOR + Class commons_Storage STORAGE_COLOR + Class commons_UI UI_COLOR + Class commons_Model MODEL_COLOR + } + +} + +together { + Package "spleetwaise.aB" <> { + Class aB_UI UI_COLOR + Class aB_Logic LOGIC_COLOR + Class aB_Storage STORAGE_COLOR + Class aB_Model MODEL_COLOR + } + + Package "spleetwaise.txn" <> { + Class txn_UI UI_COLOR + Class txn_Logic LOGIC_COLOR + Class txn_Storage STORAGE_COLOR + Class txn_Model MODEL_COLOR + } +} + +Class "<$documents>" as File UI_COLOR_T1 -UI -[#green]> Logic -UI -right[#green]-> Model -Logic -[#blue]-> Storage -Logic -down[#blue]-> Model -Main -[#grey]-> UI -Main -[#grey]-> Logic -Main -[#grey]-> Storage -Main -up[#grey]-> Model -Main -down[hidden]-> Commons +commons_UI -[#green].> commons_Logic +commons_UI -right[#green].> commons_Model +commons_Logic -[#blue].> commons_Storage +commons_Logic -down[#blue].> commons_Model +commons_Main -[#grey].> commons_UI +commons_Main -[#grey].> commons_Logic +commons_Main -[#grey].> commons_Storage +commons_Main -up[#grey].> commons_Model +commons_Storage -down[STORAGE_COLOR].> File +User -down.> commons_UI -Storage -up[STORAGE_COLOR].> Model -Storage .right[STORAGE_COLOR].>File -User ..> UI +commons -down.> aB +commons -down.> txn @enduml diff --git a/docs/diagrams/CommonModelClassDiagram.puml b/docs/diagrams/CommonModelClassDiagram.puml new file mode 100644 index 00000000000..a09f4d4c1b7 --- /dev/null +++ b/docs/diagrams/CommonModelClassDiagram.puml @@ -0,0 +1,37 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Package Model as ModelPackage <>{ +Class "<>\nAddressBookModel" as AbModel +Class "<>\nTransactionBookModel" as TbModel +Class "AddressBookModelManager" as AbModelManager +Class "TransactionBookModelManager" as TbModelManager +Class "<>\nCommonModel" as Model +Class "<>\nReadOnlyUserPrefs" as IUserPrefs +Class "UserPrefs" as UserPrefs +Class "<>\n<>\nCommonModelManager" as CommonModel + + +Class I #FFFFFF +} + +AbModelManager .up.|> AbModel +TbModelManager .up.|> TbModel +Model .down.|> AbModel +Model .down.|> TbModel + +Class HiddenOutside #FFFFFF +HiddenOutside .down.> Model + +Model .right.> IUserPrefs +UserPrefs .up.|> IUserPrefs + +CommonModel .up.|> Model +CommonModel -right-> "1" UserPrefs +CommonModel -right-> "1" TbModelManager +CommonModel -right-> "1" AbModelManager + +@enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 5241e79d7da..ab1d1c483b8 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -5,13 +5,15 @@ skinparam ArrowFontStyle plain box Logic LOGIC_COLOR_T1 participant ":LogicManager" as LogicManager LOGIC_COLOR participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":TransactionBookParser" as TransactionBookParser LOGIC_COLOR participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR participant "r:CommandResult" as CommandResult LOGIC_COLOR end box -box Model MODEL_COLOR_T1 -participant "m:Model" as Model MODEL_COLOR +box CommonModel MODEL_COLOR_T1 +participant "<> CommonModel" as CommonModel MODEL_COLOR +participant "model:CommonModel" as CommonModelInstance MODEL_COLOR end box [-> LogicManager : execute("delete 1") @@ -46,14 +48,21 @@ destroy DeleteCommandParser AddressBookParser --> LogicManager : d deactivate AddressBookParser -LogicManager -> DeleteCommand : execute(m) +LogicManager -> DeleteCommand : execute() activate DeleteCommand -DeleteCommand -> Model : deletePerson(1) -activate Model -Model --> DeleteCommand -deactivate Model +DeleteCommand -> CommonModel : getInstance() +activate CommonModel + +CommonModel --> DeleteCommand : model +deactivate CommonModel + +DeleteCommand -> CommonModelInstance : deletePerson(1) +activate CommonModelInstance + +CommonModelInstance --> DeleteCommand +deactivate CommonModelInstance create CommandResult DeleteCommand -> CommandResult diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index 58b4f602ce6..ea22477623d 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -28,7 +28,8 @@ Class HiddenOutside #FFFFFF HiddenOutside ..> Logic LogicManager .right.|> Logic -LogicManager -right->"1" ParserClasses +LogicManager -down->"1" ParserClasses +LogicManager -down->"1" ParserClasses ParserClasses ..> XYZCommand : <> XYZCommand -up-|> Command diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..eddcfa980b8 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -4,13 +4,11 @@ skinparam arrowThickness 1.1 skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR -Package Model as ModelPackage <>{ +Package "AddressBook Model" as ModelPackage <>{ Class "<>\nReadOnlyAddressBook" as ReadOnlyAddressBook -Class "<>\nReadOnlyUserPrefs" as ReadOnlyUserPrefs -Class "<>\nModel" as Model +Class "<>\nAddressBookModel" as Model Class AddressBook -Class ModelManager -Class UserPrefs +Class AddressBookModelManager Class UniquePersonList Class Person @@ -28,12 +26,9 @@ HiddenOutside ..> Model AddressBook .up.|> ReadOnlyAddressBook -ModelManager .up.|> Model -Model .right.> ReadOnlyUserPrefs +AddressBookModelManager .up.|> Model Model .left.> ReadOnlyAddressBook -ModelManager -left-> "1" AddressBook -ModelManager -right-> "1" UserPrefs -UserPrefs .up.|> ReadOnlyUserPrefs +AddressBookModelManager -left-> "1" AddressBook AddressBook *--> "1" UniquePersonList UniquePersonList --> "~* all" Person @@ -50,5 +45,5 @@ Name -[hidden]right-> Phone Phone -[hidden]right-> Address Address -[hidden]right-> Email -ModelManager --> "~* filtered" Person +AddressBookModelManager --> "~* filtered" Person @enduml diff --git a/docs/diagrams/ParserClasses.puml b/docs/diagrams/ParserClasses.puml index ce4c5ce8c8d..c3e6731aa33 100644 --- a/docs/diagrams/ParserClasses.puml +++ b/docs/diagrams/ParserClasses.puml @@ -5,34 +5,54 @@ skinparam arrowColor LOGIC_COLOR_T4 skinparam classBackgroundColor LOGIC_COLOR Class "{abstract}\nCommand" as Command -Class XYZCommand +Class AbCommand +Class TbCommand package "Parser classes"{ Class "<>\nParser" as Parser Class AddressBookParser -Class XYZCommandParser +Class TransactionBookParser +Class AbCommandParser +Class TbCommandParser Class CliSyntax Class ParserUtil Class ArgumentMultimap Class ArgumentTokenizer Class Prefix +Class Pattern } Class HiddenOutside #FFFFFF -HiddenOutside ..> AddressBookParser +HiddenOutside --> AddressBookParser +HiddenOutside --> TransactionBookParser -AddressBookParser .down.> XYZCommandParser: <> - -XYZCommandParser ..> XYZCommand : <> -AddressBookParser ..> Command : <> -XYZCommandParser .up.|> Parser -XYZCommandParser ..> ArgumentMultimap -XYZCommandParser ..> ArgumentTokenizer ArgumentTokenizer .left.> ArgumentMultimap -XYZCommandParser ..> CliSyntax CliSyntax ..> Prefix -XYZCommandParser ..> ParserUtil +AbCommandParser ..> ParserUtil ParserUtil .down.> Prefix ArgumentTokenizer .down.> Prefix -XYZCommand -up-|> Command +AbCommand -up-|> Command + +AddressBookParser -left-> Pattern +AddressBookParser .down.> AbCommandParser: <> +AbCommandParser ..> AbCommand : <> +AddressBookParser .left.> Command : <> +AbCommandParser .up.|> Parser +AbCommandParser ..> ArgumentMultimap +AbCommandParser ..> ArgumentTokenizer +AbCommandParser ..> CliSyntax + +TransactionBookParser -right-> Pattern +TransactionBookParser .down.> TbCommandParser: <> +TbCommandParser ..> TbCommand : <> +TransactionBookParser .right.> Command : <> +TbCommandParser .up.|> Parser +TbCommandParser ..> ArgumentMultimap +TbCommandParser ..> ArgumentTokenizer +TbCommandParser ..> CliSyntax +TbCommandParser ..> ParserUtil +TbCommand -up-|> Command + +'For formatting purposes: +Pattern -down[hidden]- Parser @enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..b9aae424cf6 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -4,15 +4,30 @@ skinparam arrowThickness 1.1 skinparam arrowColor STORAGE_COLOR skinparam classBackgroundColor STORAGE_COLOR + +package Model <> { +Class HiddenModel #FFFFFF +} + package Storage as StoragePackage { +Class "<>\nStorage" as Storage +Class "<>\nStorageManager" as StorageManager + +package "TransactionBook Storage" #F4F6F6{ +Class "<>\nTransactionBookStorage" as TransactionBookStorage +Class JsonTransactionBookStorage +Class JsonSerializableTransactionBook +Class JsonAdaptedTransaction +Class JsonAdaptedAmount +Class JsonAdaptedCategory +} + package "UserPrefs Storage" #F4F6F6{ Class "<>\nUserPrefsStorage" as UserPrefsStorage Class JsonUserPrefsStorage } -Class "<>\nStorage" as Storage -Class StorageManager package "AddressBook Storage" #F4F6F6{ Class "<>\nAddressBookStorage" as AddressBookStorage @@ -22,22 +37,32 @@ Class JsonAdaptedPerson Class JsonAdaptedTag } -} - Class HiddenOutside #FFFFFF HiddenOutside ..> Storage +StorageManager -right-> "1" AddressBookStorage +StorageManager -right-> "1" UserPrefsStorage +StorageManager --> "1" TransactionBookStorage StorageManager .up.|> Storage -StorageManager -up-> "1" UserPrefsStorage -StorageManager -up-> "1" AddressBookStorage -Storage -left-|> UserPrefsStorage -Storage -right-|> AddressBookStorage +Storage --|> AddressBookStorage +Storage --|> UserPrefsStorage +Storage --|> TransactionBookStorage +Storage ..> Model JsonUserPrefsStorage .up.|> UserPrefsStorage + JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonTransactionBookStorage .up.|> TransactionBookStorage +JsonTransactionBookStorage ..> JsonSerializableTransactionBook +JsonSerializableTransactionBook --> "*" JsonAdaptedTransaction +JsonAdaptedTransaction --> "1" JsonAdaptedAmount +JsonAdaptedTransaction --> "*" JsonAdaptedCategory + + + @enduml diff --git a/docs/diagrams/TransactionModelClassDiagram.puml b/docs/diagrams/TransactionModelClassDiagram.puml new file mode 100644 index 00000000000..314f07677c9 --- /dev/null +++ b/docs/diagrams/TransactionModelClassDiagram.puml @@ -0,0 +1,42 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Package "TransactionBook Model" as ModelPackage <>{ +Class "<>\nReadOnlyTransactionBook" as ReadOnlyTransactionBook +Class "<>\nTransactionBookModel" as TransactionBookModel +Class TransactionBook +Class TransactionBookModelManager + +Class Transaction +Class Amount +Class Date +Class Description +Class Person +Class Status +Class Category + +Class I #FFFFFF +} + +Class HiddenOutside #FFFFFF +HiddenOutside ..> TransactionBookModel + +TransactionBook .up.|> ReadOnlyTransactionBook + +TransactionBookModelManager .up.|> TransactionBookModel +TransactionBookModel .left.> ReadOnlyTransactionBook +TransactionBookModelManager -left-> "1" TransactionBook + +TransactionBook *--> "~* all" Transaction +Transaction *--> Person +Transaction *--> Amount +Transaction *--> Description +Transaction *--> Date +Transaction *--> Status +Transaction *--> "~*" Category + +TransactionBookModelManager --> "~* filtered" Transaction +@enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..870e575935e 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -15,10 +15,9 @@ Class PersonListPanel Class PersonCard Class StatusBarFooter Class CommandBox -} - -package Model <> { -Class HiddenModel #FFFFFF +Class TransactionCard +Class TransactionListPanel +Class RightPanel } package Logic <> { @@ -34,10 +33,14 @@ MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel MainWindow *-down-> "1" StatusBarFooter +MainWindow *-down-> "1" RightPanel MainWindow --> "0..1" HelpWindow PersonListPanel -down-> "*" PersonCard +RightPanel -down-> "1" TransactionListPanel +TransactionListPanel -down-> "*" TransactionCard + MainWindow -left-|> UiPart ResultDisplay --|> UiPart @@ -46,8 +49,10 @@ PersonListPanel --|> UiPart PersonCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +RightPanel --|> UiPart +TransactionListPanel --|> UiPart +TransactionCard --|> UiPart -PersonCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic diff --git a/docs/diagrams/tracing/LogicSequenceDiagram.puml b/docs/diagrams/tracing/LogicSequenceDiagram.puml index 42bf46d3ce8..e04a8a558cb 100644 --- a/docs/diagrams/tracing/LogicSequenceDiagram.puml +++ b/docs/diagrams/tracing/LogicSequenceDiagram.puml @@ -4,19 +4,23 @@ skinparam ArrowFontStyle plain Participant ":LogicManager" as logic LOGIC_COLOR Participant ":AddressBookParser" as abp LOGIC_COLOR -Participant ":EditCommandParser" as ecp LOGIC_COLOR -Participant "command:EditCommand" as ec LOGIC_COLOR +Participant ":TransactionBookParser" as tbp LOGIC_COLOR +Participant ":AddCommandParser" as acp LOGIC_COLOR +Participant ":command:AddCommand" as ac LOGIC_COLOR [-> logic : execute activate logic logic -> abp ++: parseCommand(commandText) -create ecp -abp -> ecp -abp -> ecp ++: parse(arguments) -create ec -ecp -> ec ++: index, editPersonDescriptor -ec --> ecp -- -ecp --> abp --: command -abp --> logic --: command +abp --> logic --: null + +logic -> tbp ++: parseCommand(commandText) +create acp +tbp -> acp +tbp -> acp ++: parse(arguments) +create ac +acp -> ac ++: transaction +ac --> acp -- +acp --> tbp --: command +tbp --> logic --: command @enduml diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png index cd540665053..d1066e4766b 100644 Binary files a/docs/images/ArchitectureDiagram.png and b/docs/images/ArchitectureDiagram.png differ diff --git a/docs/images/CommonModelClassDiagram.png b/docs/images/CommonModelClassDiagram.png new file mode 100644 index 00000000000..1d466d54d0f Binary files /dev/null and b/docs/images/CommonModelClassDiagram.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index ac2ae217c51..bd880000565 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index fe91c69efe7..afbbd7b41e5 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/LogicSequenceDiagram.png b/docs/images/LogicSequenceDiagram.png new file mode 100644 index 00000000000..48c741fee3e Binary files /dev/null and b/docs/images/LogicSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..dd92bf116eb 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png index 2caeeb1a067..e9aef7d1d8c 100644 Binary files a/docs/images/ParserClasses.png and b/docs/images/ParserClasses.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..c05fe7b46a0 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/TransactionModelClassDiagram.png b/docs/images/TransactionModelClassDiagram.png new file mode 100644 index 00000000000..03338dd528c Binary files /dev/null and b/docs/images/TransactionModelClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..bb8a2ebbe19 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..c61e8649028 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/dino-nuggies.png b/docs/images/dino-nuggies.png new file mode 100644 index 00000000000..972fe84ee96 Binary files /dev/null and b/docs/images/dino-nuggies.png differ diff --git a/docs/images/filterTxnAlexYeohResult.png b/docs/images/filterTxnAlexYeohResult.png new file mode 100644 index 00000000000..1d5494ba848 Binary files /dev/null and b/docs/images/filterTxnAlexYeohResult.png differ diff --git a/docs/images/filterTxnBerniceYuAmt55Result.png b/docs/images/filterTxnBerniceYuAmt55Result.png new file mode 100644 index 00000000000..164ca22ba6f Binary files /dev/null and b/docs/images/filterTxnBerniceYuAmt55Result.png differ diff --git a/docs/images/filterTxnExample.png b/docs/images/filterTxnExample.png new file mode 100644 index 00000000000..9026e94af64 Binary files /dev/null and b/docs/images/filterTxnExample.png differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png index 235da1c273e..8edc3ebb113 100644 Binary files a/docs/images/findAlexDavidResult.png and b/docs/images/findAlexDavidResult.png differ diff --git a/docs/images/gavinsin.png b/docs/images/gavinsin.png new file mode 100644 index 00000000000..f4d7da7f686 Binary files /dev/null and b/docs/images/gavinsin.png differ diff --git a/docs/images/gohyongjing.png b/docs/images/gohyongjing.png new file mode 100644 index 00000000000..a2f17ea77bb Binary files /dev/null and b/docs/images/gohyongjing.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..4af811effbc 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/rollingpencil.png b/docs/images/rollingpencil.png new file mode 100644 index 00000000000..ac8364011a3 Binary files /dev/null and b/docs/images/rollingpencil.png differ diff --git a/docs/images/seanlim.png b/docs/images/seanlim.png new file mode 100644 index 00000000000..972fe84ee96 Binary files /dev/null and b/docs/images/seanlim.png differ diff --git a/docs/images/seeyangzhi.png b/docs/images/seeyangzhi.png new file mode 100644 index 00000000000..972fe84ee96 Binary files /dev/null and b/docs/images/seeyangzhi.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..9aa2eee097b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,22 @@ --- layout: page -title: AddressBook Level-3 +title: SpleetWaise --- -[![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/AY2425S1-CS2103-F13-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2425S1-CS2103-F13-1/tp/actions?query=branch%3Amaster) -![Ui](images/Ui.png) +[![codecov](https://codecov.io/gh/AY2425S1-CS2103-F13-1/tp/graph/badge.svg?token=91MOH0UZHU)](https://codecov.io/gh/AY2425S1-CS2103-F13-1/tp) -**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). +![Ui](images/Ui.png) -* 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. +SpleetWaise builds on [AddressBook Level 3 (AB3)](https://se-education.org/addressbook-level3/), **a desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still offering the benefits of a Graphical User Interface (GUI). Designed to streamline expense tracking for students, SpleetWaise makes it easy to record and monitor both personal and shared expenses with contacts saved in the address book. With features to keep track of balances with friends, it eliminates the confusion often associated with managing shared costs, providing a clear, organised view of who has owes what. If you can type fast, SpleetWaise lets you handle your contact and expense management tasks more efficiently than traditional GUI apps, offering students a stress-free way to manage their expenses and shared balances with contacts. +- If you are interested in using SpleetWaise, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +- If you are interested about developing SpleetWaise, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** -* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) +- Libraries + used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5), [Mockito](https://github.com/mockito/mockito), [TestFX](https://github.com/TestFX/TestFX) +- References + used: [SE-EDU initiative](https://se-education.org/), [AB4](https://github.com/se-edu/addressbook-level4) diff --git a/docs/team/johndoe.md b/docs/team/seeyangzhi.md similarity index 100% rename from docs/team/johndoe.md rename to docs/team/seeyangzhi.md diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java deleted file mode 100644 index 678ddc8c218..00000000000 --- a/src/main/java/seedu/address/MainApp.java +++ /dev/null @@ -1,186 +0,0 @@ -package seedu.address; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Logger; - -import javafx.application.Application; -import javafx.stage.Stage; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.core.Version; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.commons.util.ConfigUtil; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; -import seedu.address.logic.LogicManager; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; -import seedu.address.model.util.SampleDataUtil; -import seedu.address.storage.AddressBookStorage; -import seedu.address.storage.JsonAddressBookStorage; -import seedu.address.storage.JsonUserPrefsStorage; -import seedu.address.storage.Storage; -import seedu.address.storage.StorageManager; -import seedu.address.storage.UserPrefsStorage; -import seedu.address.ui.Ui; -import seedu.address.ui.UiManager; - -/** - * Runs the application. - */ -public class MainApp extends Application { - - public static final Version VERSION = new Version(0, 2, 2, true); - - private static final Logger logger = LogsCenter.getLogger(MainApp.class); - - protected Ui ui; - protected Logic logic; - protected Storage storage; - protected Model model; - protected Config config; - - @Override - public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); - super.init(); - - AppParameters appParameters = AppParameters.parse(getParameters()); - config = initConfig(appParameters.getConfigPath()); - initLogging(config); - - UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); - UserPrefs userPrefs = initPrefs(userPrefsStorage); - AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); - - model = initModelManager(storage, userPrefs); - - logic = new LogicManager(model, storage); - - ui = new UiManager(logic); - } - - /** - * Returns a {@code ModelManager} with the data from {@code storage}'s address book and {@code userPrefs}.
- * The data from the sample address book will be used instead if {@code storage}'s address book is not found, - * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. - */ - private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { - logger.info("Using data file : " + storage.getAddressBookFilePath()); - - Optional addressBookOptional; - ReadOnlyAddressBook initialData; - try { - addressBookOptional = storage.readAddressBook(); - if (!addressBookOptional.isPresent()) { - logger.info("Creating a new data file " + storage.getAddressBookFilePath() - + " populated with a sample AddressBook."); - } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); - } catch (DataLoadingException e) { - logger.warning("Data file at " + storage.getAddressBookFilePath() + " could not be loaded." - + " Will be starting with an empty AddressBook."); - initialData = new AddressBook(); - } - - return new ModelManager(initialData, userPrefs); - } - - private void initLogging(Config config) { - LogsCenter.init(config); - } - - /** - * Returns a {@code Config} using the file at {@code configFilePath}.
- * The default file path {@code Config#DEFAULT_CONFIG_FILE} will be used instead - * if {@code configFilePath} is null. - */ - protected Config initConfig(Path configFilePath) { - Config initializedConfig; - Path configFilePathUsed; - - configFilePathUsed = Config.DEFAULT_CONFIG_FILE; - - if (configFilePath != null) { - logger.info("Custom Config file specified " + configFilePath); - configFilePathUsed = configFilePath; - } - - logger.info("Using config file : " + configFilePathUsed); - - try { - Optional configOptional = ConfigUtil.readConfig(configFilePathUsed); - if (!configOptional.isPresent()) { - logger.info("Creating new config file " + configFilePathUsed); - } - initializedConfig = configOptional.orElse(new Config()); - } catch (DataLoadingException e) { - logger.warning("Config file at " + configFilePathUsed + " could not be loaded." - + " Using default config properties."); - initializedConfig = new Config(); - } - - //Update config file in case it was missing to begin with or there are new/unused fields - try { - ConfigUtil.saveConfig(initializedConfig, configFilePathUsed); - } catch (IOException e) { - logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); - } - return initializedConfig; - } - - /** - * Returns a {@code UserPrefs} using the file at {@code storage}'s user prefs file path, - * or a new {@code UserPrefs} with default configuration if errors occur when - * reading from the file. - */ - protected UserPrefs initPrefs(UserPrefsStorage storage) { - Path prefsFilePath = storage.getUserPrefsFilePath(); - logger.info("Using preference file : " + prefsFilePath); - - UserPrefs initializedPrefs; - try { - Optional prefsOptional = storage.readUserPrefs(); - if (!prefsOptional.isPresent()) { - logger.info("Creating new preference file " + prefsFilePath); - } - initializedPrefs = prefsOptional.orElse(new UserPrefs()); - } catch (DataLoadingException e) { - logger.warning("Preference file at " + prefsFilePath + " could not be loaded." - + " Using default preferences."); - initializedPrefs = new UserPrefs(); - } - - //Update prefs file in case it was missing to begin with or there are new/unused fields - try { - storage.saveUserPrefs(initializedPrefs); - } catch (IOException e) { - logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); - } - - return initializedPrefs; - } - - @Override - public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); - ui.start(primaryStage); - } - - @Override - public void stop() { - logger.info("============================ [ Stopping AddressBook ] ============================="); - try { - storage.saveUserPrefs(model.getUserPrefs()); - } catch (IOException e) { - logger.severe("Failed to save preferences " + StringUtil.getDetails(e)); - } - } -} diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java deleted file mode 100644 index 5aa3b91c7d0..00000000000 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ /dev/null @@ -1,88 +0,0 @@ -package seedu.address.logic; - -import java.io.IOException; -import java.nio.file.AccessDeniedException; -import java.nio.file.Path; -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.Model; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; -import seedu.address.storage.Storage; - -/** - * The main LogicManager of the app. - */ -public class LogicManager implements Logic { - public static final String FILE_OPS_ERROR_FORMAT = "Could not save data due to the following error: %s"; - - public static final String FILE_OPS_PERMISSION_ERROR_FORMAT = - "Could not save data to file %s due to insufficient permissions to write to the file or the folder."; - - private final Logger logger = LogsCenter.getLogger(LogicManager.class); - - private final Model model; - private final Storage storage; - private final AddressBookParser addressBookParser; - - /** - * Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}. - */ - public LogicManager(Model model, Storage storage) { - this.model = model; - this.storage = storage; - addressBookParser = new AddressBookParser(); - } - - @Override - public CommandResult execute(String commandText) throws CommandException, ParseException { - logger.info("----------------[USER COMMAND][" + commandText + "]"); - - CommandResult commandResult; - Command command = addressBookParser.parseCommand(commandText); - commandResult = command.execute(model); - - try { - storage.saveAddressBook(model.getAddressBook()); - } catch (AccessDeniedException e) { - throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e); - } catch (IOException ioe) { - throw new CommandException(String.format(FILE_OPS_ERROR_FORMAT, ioe.getMessage()), ioe); - } - - return commandResult; - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return model.getAddressBook(); - } - - @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); - } - - @Override - public Path getAddressBookFilePath() { - return model.getAddressBookFilePath(); - } - - @Override - public GuiSettings getGuiSettings() { - return model.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - model.setGuiSettings(guiSettings); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java deleted file mode 100644 index 9c86b1fa6e4..00000000000 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ /dev/null @@ -1,23 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.model.AddressBook; -import seedu.address.model.Model; - -/** - * Clears the address book. - */ -public class ClearCommand extends Command { - - public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.setAddressBook(new AddressBook()); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java deleted file mode 100644 index 84be6ad2596..00000000000 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ /dev/null @@ -1,24 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import seedu.address.model.Model; - -/** - * Lists all persons in the address book to the user. - */ -public class ListCommand extends Command { - - public static final String COMMAND_WORD = "list"; - - public static final String MESSAGE_SUCCESS = "Listed all persons"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java deleted file mode 100644 index a16bd14f2cd..00000000000 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ /dev/null @@ -1,17 +0,0 @@ -package seedu.address.logic.commands.exceptions; - -/** - * Represents an error which occurs during execution of a {@link Command}. - */ -public class CommandException extends Exception { - public CommandException(String message) { - super(message); - } - - /** - * Constructs a new {@code CommandException} with the specified detail {@code message} and {@code cause}. - */ - public CommandException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java deleted file mode 100644 index 4ff1a97ed77..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ /dev/null @@ -1,61 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Set; -import java.util.stream.Stream; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new AddCommand object - */ -public class AddCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the AddCommand - * and returns an AddCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public AddCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); - } - - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); - } - -} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java deleted file mode 100644 index fa764426ca7..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ /dev/null @@ -1,6 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation is unable to find the specified person. - */ -public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java deleted file mode 100644 index 1806da4facf..00000000000 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.model.util; - -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods for populating {@code AddressBook} with sample data. - */ -public class SampleDataUtil { - public static Person[] getSamplePersons() { - return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) - }; - } - - public static ReadOnlyAddressBook getSampleAddressBook() { - AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); - } - return sampleAb; - } - - /** - * Returns a tag set containing the list of strings given. - */ - public static Set getTagSet(String... strings) { - return Arrays.stream(strings) - .map(Tag::new) - .collect(Collectors.toSet()); - } - -} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java deleted file mode 100644 index 9fba0c7a1d6..00000000000 --- a/src/main/java/seedu/address/storage/Storage.java +++ /dev/null @@ -1,32 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; - -/** - * API of the Storage component - */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { - - @Override - Optional readUserPrefs() throws DataLoadingException; - - @Override - void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; - - @Override - Path getAddressBookFilePath(); - - @Override - Optional readAddressBook() throws DataLoadingException; - - @Override - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - -} diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/spleetwaise/address/logic/Messages.java similarity index 84% rename from src/main/java/seedu/address/logic/Messages.java rename to src/main/java/spleetwaise/address/logic/Messages.java index ecd32c31b53..a49df37d7c2 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/spleetwaise/address/logic/Messages.java @@ -1,11 +1,11 @@ -package seedu.address.logic; +package spleetwaise.address.logic; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import seedu.address.logic.parser.Prefix; -import seedu.address.model.person.Person; +import spleetwaise.address.logic.parser.Prefix; +import spleetwaise.address.model.person.Person; /** * Container for user visible messages. @@ -17,7 +17,7 @@ public class Messages { public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = - "Multiple values specified for the following single-valued field(s): "; + "Multiple values specified for the following single-valued field(s): "; /** * Returns an error message indicating the duplicate prefixes. @@ -43,6 +43,8 @@ public static String format(Person person) { .append(person.getEmail()) .append("; Address: ") .append(person.getAddress()) + .append("; Remark: ") + .append(person.getRemark()) .append("; Tags: "); person.getTags().forEach(builder::append); return builder.toString(); diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/spleetwaise/address/logic/commands/AddCommand.java similarity index 61% rename from src/main/java/seedu/address/logic/commands/AddCommand.java rename to src/main/java/spleetwaise/address/logic/commands/AddCommand.java index 5d7185a9680..ac12fa6895a 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/spleetwaise/address/logic/commands/AddCommand.java @@ -1,17 +1,20 @@ -package seedu.address.logic.commands; +package spleetwaise.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import seedu.address.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; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_NAME; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_REMARK; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_TAG; + +import spleetwaise.address.logic.Messages; +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.commons.util.ToStringBuilder; /** * Adds a person to the address book. @@ -26,12 +29,14 @@ public class AddCommand extends Command { + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " + + "[" + PREFIX_REMARK + "REMARK] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "98765432 " + PREFIX_EMAIL + "johnd@example.com " + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " + + PREFIX_REMARK + "This guy owes me money " + PREFIX_TAG + "friends " + PREFIX_TAG + "owesMoney"; @@ -49,8 +54,8 @@ public AddCommand(Person person) { } @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); + public CommandResult execute() throws CommandException { + CommonModelManager model = CommonModelManager.getInstance(); if (model.hasPerson(toAdd)) { throw new CommandException(MESSAGE_DUPLICATE_PERSON); @@ -67,11 +72,10 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof AddCommand)) { + if (!(other instanceof AddCommand otherAddCommand)) { return false; } - AddCommand otherAddCommand = (AddCommand) other; return toAdd.equals(otherAddCommand.toAdd); } @@ -81,4 +85,8 @@ public String toString() { .add("toAdd", toAdd) .toString(); } + + public Person getPerson() { + return toAdd; + } } diff --git a/src/main/java/spleetwaise/address/logic/commands/ClearCommand.java b/src/main/java/spleetwaise/address/logic/commands/ClearCommand.java new file mode 100644 index 00000000000..921c8d74eb0 --- /dev/null +++ b/src/main/java/spleetwaise/address/logic/commands/ClearCommand.java @@ -0,0 +1,25 @@ +package spleetwaise.address.logic.commands; + +import spleetwaise.address.model.AddressBook; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.transaction.model.TransactionBook; + +/** + * Clears the address book. + */ +public class ClearCommand extends Command { + + public static final String COMMAND_WORD = "clear"; + public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + + + @Override + public CommandResult execute() { + CommonModelManager model = CommonModelManager.getInstance(); + model.setAddressBook(new AddressBook()); + model.setTransactionBook(new TransactionBook()); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/spleetwaise/address/logic/commands/DeleteCommand.java similarity index 62% rename from src/main/java/seedu/address/logic/commands/DeleteCommand.java rename to src/main/java/spleetwaise/address/logic/commands/DeleteCommand.java index 1135ac19b74..34c734517f5 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/spleetwaise/address/logic/commands/DeleteCommand.java @@ -1,15 +1,15 @@ -package seedu.address.logic.commands; +package spleetwaise.address.logic.commands; -import static java.util.Objects.requireNonNull; +import static spleetwaise.commons.logic.commands.CommandUtil.getPersonByFilteredPersonListIndex; -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; +import spleetwaise.address.logic.Messages; +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.commons.util.ToStringBuilder; /** * Deletes a person identified using it's displayed index from the address book. @@ -32,16 +32,12 @@ public DeleteCommand(Index targetIndex) { } @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } + public CommandResult execute() throws CommandException { + CommonModelManager model = CommonModelManager.getInstance(); - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); + Person personToDelete = getPersonByFilteredPersonListIndex(model, targetIndex); model.deletePerson(personToDelete); + model.deleteTransactionsOfPersonId(personToDelete.getId()); return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/spleetwaise/address/logic/commands/EditCommand.java similarity index 59% rename from src/main/java/seedu/address/logic/commands/EditCommand.java rename to src/main/java/spleetwaise/address/logic/commands/EditCommand.java index 4b581c7331e..5438b96f2dd 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/spleetwaise/address/logic/commands/EditCommand.java @@ -1,32 +1,37 @@ -package seedu.address.logic.commands; +package spleetwaise.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_NAME; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_REMARK; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_TAG; +import static spleetwaise.commons.logic.commands.CommandUtil.getPersonByFilteredPersonListIndex; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; -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.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import spleetwaise.address.logic.Messages; +import spleetwaise.address.model.AddressBookModel; +import spleetwaise.address.model.person.Address; +import spleetwaise.address.model.person.Email; +import spleetwaise.address.model.person.Name; +import spleetwaise.address.model.person.Person; +import spleetwaise.address.model.person.Phone; +import spleetwaise.address.model.person.Remark; +import spleetwaise.address.model.tag.Tag; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.commons.util.CollectionUtil; +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Transaction; /** * Edits the details of an existing person in the address book. @@ -43,6 +48,7 @@ public class EditCommand extends Command { + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_REMARK + "REMARK] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " @@ -56,7 +62,7 @@ public class EditCommand extends Command { private final EditPersonDescriptor editPersonDescriptor; /** - * @param index of the person in the filtered person list to edit + * @param index of the person in the filtered person list to edit * @param editPersonDescriptor details to edit the person with */ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { @@ -67,16 +73,33 @@ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); } - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} edited with + * {@code editPersonDescriptor}. + */ + private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { + assert personToEdit != null; - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } + String id = editPersonDescriptor.getId().orElse(personToEdit.getId()); + Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); + Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); + Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); + Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + Remark updatedRemark = editPersonDescriptor.getRemark().orElse(personToEdit.getRemark()); + Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + + return new Person(id, updatedName, updatedPhone, updatedEmail, updatedAddress, updatedRemark, updatedTags); + } + + public EditPersonDescriptor getDescriptor() { + return editPersonDescriptor; + } + + @Override + public CommandResult execute() throws CommandException { + CommonModelManager model = CommonModelManager.getInstance(); - Person personToEdit = lastShownList.get(index.getZeroBased()); + Person personToEdit = getPersonByFilteredPersonListIndex(model, index); Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { @@ -84,24 +107,27 @@ public CommandResult execute(Model model) throws CommandException { } model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); - } + model.updateFilteredPersonList(AddressBookModel.PREDICATE_SHOW_ALL_PERSONS); - /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. - */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; + updatePersonsInTransactions(model, personToEdit, editedPerson); - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); + } - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + private void updatePersonsInTransactions( + CommonModelManager model, Person oldPerson, + Person updatedPerson + ) { + requireNonNull(model); + requireNonNull(oldPerson); + requireNonNull(updatedPerson); + for (Transaction txn : model.getFilteredTransactionList()) { + if (txn.getPerson().equals(oldPerson)) { + model.setTransaction(txn, new Transaction(txn.getId(), updatedPerson, txn.getAmount(), + txn.getDescription(), txn.getDate(), txn.getCategories(), txn.getStatus() + )); + } + } } @Override @@ -111,11 +137,10 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof EditCommand)) { + if (!(other instanceof EditCommand otherEditCommand)) { return false; } - EditCommand otherEditCommand = (EditCommand) other; return index.equals(otherEditCommand.index) && editPersonDescriptor.equals(otherEditCommand.editPersonDescriptor); } @@ -129,27 +154,31 @@ public String toString() { } /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. + * Stores the details to edit the person with. Each non-empty field value will replace the corresponding field value + * of the person. */ public static class EditPersonDescriptor { + private String id; private Name name; private Phone phone; private Email email; private Address address; + private Remark remark; private Set tags; - public EditPersonDescriptor() {} + public EditPersonDescriptor() { + } /** - * Copy constructor. - * A defensive copy of {@code tags} is used internally. + * Copy constructor. A defensive copy of {@code tags} is used internally. */ public EditPersonDescriptor(EditPersonDescriptor toCopy) { + setId(toCopy.id); setName(toCopy.name); setPhone(toCopy.phone); setEmail(toCopy.email); setAddress(toCopy.address); + setRemark(toCopy.remark); setTags(toCopy.tags); } @@ -157,58 +186,72 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, address, remark, tags); } - public void setName(Name name) { - this.name = name; + public Optional getId() { + return Optional.ofNullable(id); + } + + public void setId(String id) { + this.id = id; } public Optional getName() { return Optional.ofNullable(name); } - public void setPhone(Phone phone) { - this.phone = phone; + public void setName(Name name) { + this.name = name; } public Optional getPhone() { return Optional.ofNullable(phone); } - public void setEmail(Email email) { - this.email = email; + public void setPhone(Phone phone) { + this.phone = phone; } public Optional getEmail() { return Optional.ofNullable(email); } - public void setAddress(Address address) { - this.address = address; + public void setEmail(Email email) { + this.email = email; } public Optional
getAddress() { return Optional.ofNullable(address); } - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; + public void setAddress(Address address) { + this.address = address; + } + + public Optional getRemark() { + return Optional.ofNullable(remark); + } + + public void setRemark(Remark remark) { + this.remark = remark; } /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} if modification is + * attempted. Returns {@code Optional#empty()} if {@code tags} is null. */ public Optional> getTags() { return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); } + /** + * Sets {@code tags} to this object's {@code tags}. A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -216,15 +259,16 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { + if (!(other instanceof EditPersonDescriptor otherEditPersonDescriptor)) { return false; } - EditPersonDescriptor otherEditPersonDescriptor = (EditPersonDescriptor) other; - return Objects.equals(name, otherEditPersonDescriptor.name) + return Objects.equals(id, otherEditPersonDescriptor.id) + && Objects.equals(name, otherEditPersonDescriptor.name) && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) && Objects.equals(address, otherEditPersonDescriptor.address) + && Objects.equals(remark, otherEditPersonDescriptor.remark) && Objects.equals(tags, otherEditPersonDescriptor.tags); } @@ -235,6 +279,7 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("remark", remark) .add("tags", tags) .toString(); } diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/spleetwaise/address/logic/commands/ExitCommand.java similarity index 64% rename from src/main/java/seedu/address/logic/commands/ExitCommand.java rename to src/main/java/spleetwaise/address/logic/commands/ExitCommand.java index 3dd85a8ba90..2d32988a6e9 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/spleetwaise/address/logic/commands/ExitCommand.java @@ -1,6 +1,7 @@ -package seedu.address.logic.commands; +package spleetwaise.address.logic.commands; -import seedu.address.model.Model; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; /** * Terminates the program. @@ -12,7 +13,7 @@ public class ExitCommand extends Command { public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; @Override - public CommandResult execute(Model model) { + public CommandResult execute() { return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); } diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/spleetwaise/address/logic/commands/FindCommand.java similarity index 72% rename from src/main/java/seedu/address/logic/commands/FindCommand.java rename to src/main/java/spleetwaise/address/logic/commands/FindCommand.java index 72b9eddd3a7..732ee7420af 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/spleetwaise/address/logic/commands/FindCommand.java @@ -1,15 +1,15 @@ -package seedu.address.logic.commands; +package spleetwaise.address.logic.commands; -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.util.ToStringBuilder; -import seedu.address.logic.Messages; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import spleetwaise.address.logic.Messages; +import spleetwaise.address.model.person.NameContainsKeywordsPredicate; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.commons.util.ToStringBuilder; /** - * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. + * Finds and lists all persons in address book whose name contains any of the argument keywords. Keyword matching is + * case insensitive. */ public class FindCommand extends Command { @@ -27,8 +27,9 @@ public FindCommand(NameContainsKeywordsPredicate predicate) { } @Override - public CommandResult execute(Model model) { - requireNonNull(model); + public CommandResult execute() { + CommonModelManager model = CommonModelManager.getInstance(); + model.updateFilteredPersonList(predicate); 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/spleetwaise/address/logic/commands/HelpCommand.java similarity index 72% rename from src/main/java/seedu/address/logic/commands/HelpCommand.java rename to src/main/java/spleetwaise/address/logic/commands/HelpCommand.java index bf824f91bd0..e903d615107 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/spleetwaise/address/logic/commands/HelpCommand.java @@ -1,6 +1,7 @@ -package seedu.address.logic.commands; +package spleetwaise.address.logic.commands; -import seedu.address.model.Model; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; /** * Format full help instructions for every command for display. @@ -15,7 +16,7 @@ public class HelpCommand extends Command { public static final String SHOWING_HELP_MESSAGE = "Opened help window."; @Override - public CommandResult execute(Model model) { + public CommandResult execute() { return new CommandResult(SHOWING_HELP_MESSAGE, true, false); } } diff --git a/src/main/java/spleetwaise/address/logic/commands/ListCommand.java b/src/main/java/spleetwaise/address/logic/commands/ListCommand.java new file mode 100644 index 00000000000..bf3886fc1bb --- /dev/null +++ b/src/main/java/spleetwaise/address/logic/commands/ListCommand.java @@ -0,0 +1,26 @@ +package spleetwaise.address.logic.commands; + +import static spleetwaise.address.model.AddressBookModel.PREDICATE_SHOW_ALL_PERSONS; + +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.model.CommonModelManager; + +/** + * Lists all persons in the address book to the user. + */ +public class ListCommand extends Command { + + public static final String COMMAND_WORD = "list"; + + public static final String MESSAGE_SUCCESS = "Listed all persons"; + + + @Override + public CommandResult execute() { + CommonModelManager model = CommonModelManager.getInstance(); + + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/spleetwaise/address/logic/commands/RemarkCommand.java b/src/main/java/spleetwaise/address/logic/commands/RemarkCommand.java new file mode 100644 index 00000000000..8640f37b7e8 --- /dev/null +++ b/src/main/java/spleetwaise/address/logic/commands/RemarkCommand.java @@ -0,0 +1,84 @@ +package spleetwaise.address.logic.commands; + +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_REMARK; +import static spleetwaise.address.model.AddressBookModel.PREDICATE_SHOW_ALL_PERSONS; +import static spleetwaise.commons.logic.commands.CommandUtil.getPersonByFilteredPersonListIndex; +import static spleetwaise.commons.util.CollectionUtil.requireAllNonNull; + +import spleetwaise.address.model.person.Person; +import spleetwaise.address.model.person.Remark; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.model.CommonModelManager; + +/** + * Changes the remark of an existing person in the address book. + */ +public class RemarkCommand extends Command { + + public static final String COMMAND_WORD = "remark"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the remark of the person identified " + + "by the index number used in the last person listing. " + + "Existing remark will be overwritten by the input.\n" + "Parameters: INDEX (must be a positive integer) " + + PREFIX_REMARK + "[REMARK]\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_REMARK + "Likes to swim."; + public static final String MESSAGE_ADD_REMARK_SUCCESS = "Added remark to Person: %1$s"; + public static final String MESSAGE_DELETE_REMARK_SUCCESS = "Removed remark from Person: %1$s"; + + private final Index index; + private final Remark remark; + + /** + * @param index of the person in the filtered person list to edit the remark + * @param remark of the person to be updated to + */ + public RemarkCommand(Index index, Remark remark) { + requireAllNonNull(index, remark); + + this.index = index; + this.remark = remark; + } + + @Override + public CommandResult execute() throws CommandException { + CommonModelManager model = CommonModelManager.getInstance(); + + Person personToEdit = getPersonByFilteredPersonListIndex(model, index); + + Person editedPerson = new Person(personToEdit.getId(), personToEdit.getName(), personToEdit.getPhone(), + personToEdit.getEmail(), personToEdit.getAddress(), remark, personToEdit.getTags() + ); + + model.setPerson(personToEdit, editedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + return new CommandResult(generateSuccessMessage(editedPerson)); + } + + /** + * Generates a command execution success message based on whether the remark is added to or removed from + * {@code personToEdit}. + */ + private String generateSuccessMessage(Person personToEdit) { + String message = !remark.value.isEmpty() ? MESSAGE_ADD_REMARK_SUCCESS : MESSAGE_DELETE_REMARK_SUCCESS; + return String.format(message, personToEdit); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof RemarkCommand e)) { + return false; + } + + // state check + return index.equals(e.index) && remark.equals(e.remark); + } +} diff --git a/src/main/java/spleetwaise/address/logic/parser/AddCommandParser.java b/src/main/java/spleetwaise/address/logic/parser/AddCommandParser.java new file mode 100644 index 00000000000..d64d4f0e9d7 --- /dev/null +++ b/src/main/java/spleetwaise/address/logic/parser/AddCommandParser.java @@ -0,0 +1,62 @@ +package spleetwaise.address.logic.parser; + +import static spleetwaise.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_NAME; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_REMARK; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Optional; +import java.util.Set; + +import spleetwaise.address.logic.commands.AddCommand; +import spleetwaise.address.model.person.Address; +import spleetwaise.address.model.person.Email; +import spleetwaise.address.model.person.Name; +import spleetwaise.address.model.person.Person; +import spleetwaise.address.model.person.Phone; +import spleetwaise.address.model.person.Remark; +import spleetwaise.address.model.tag.Tag; +import spleetwaise.commons.logic.parser.Parser; +import spleetwaise.commons.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new AddCommand object + */ +public class AddCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the AddCommand and returns an AddCommand object + * for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public AddCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize( + args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_REMARK, PREFIX_TAG); + + if (!ParserUtil.arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor( + PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_REMARK); + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); + Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); + Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + // To prevent users from unnecessarily adding PREFIX_REMARK with no input + Optional remarkInput = argMultimap.getValue(PREFIX_REMARK); + if (remarkInput.isPresent() && remarkInput.get().isBlank()) { + throw new ParseException(Remark.MESSAGE_CONSTRAINTS); + } + Remark remark = ParserUtil.parseRemark(remarkInput.orElse("")); + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + + Person person = new Person(name, phone, email, address, remark, tagList); + + return new AddCommand(person); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/spleetwaise/address/logic/parser/AddressBookParser.java similarity index 68% rename from src/main/java/seedu/address/logic/parser/AddressBookParser.java rename to src/main/java/spleetwaise/address/logic/parser/AddressBookParser.java index 3149ee07e0b..9af8b8251ce 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/spleetwaise/address/logic/parser/AddressBookParser.java @@ -1,23 +1,21 @@ -package seedu.address.logic.parser; - -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; +package spleetwaise.address.logic.parser; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.parser.exceptions.ParseException; +import spleetwaise.address.logic.commands.AddCommand; +import spleetwaise.address.logic.commands.ClearCommand; +import spleetwaise.address.logic.commands.DeleteCommand; +import spleetwaise.address.logic.commands.EditCommand; +import spleetwaise.address.logic.commands.ExitCommand; +import spleetwaise.address.logic.commands.FindCommand; +import spleetwaise.address.logic.commands.HelpCommand; +import spleetwaise.address.logic.commands.ListCommand; +import spleetwaise.address.logic.commands.RemarkCommand; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.parser.exceptions.ParseException; /** * Parses user input. @@ -40,7 +38,7 @@ public class AddressBookParser { public Command parseCommand(String userInput) throws ParseException { final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); if (!matcher.matches()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + return null; } final String commandWord = matcher.group("commandWord"); @@ -52,7 +50,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); @@ -71,15 +68,16 @@ public Command parseCommand(String userInput) throws ParseException { case ListCommand.COMMAND_WORD: return new ListCommand(); + case RemarkCommand.COMMAND_WORD: + return new RemarkCommandParser().parse(arguments); + case ExitCommand.COMMAND_WORD: return new ExitCommand(); case HelpCommand.COMMAND_WORD: return new HelpCommand(); - default: - logger.finer("This user input caused a ParseException: " + userInput); - throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + return null; } } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/spleetwaise/address/logic/parser/ArgumentMultimap.java similarity index 67% rename from src/main/java/seedu/address/logic/parser/ArgumentMultimap.java rename to src/main/java/spleetwaise/address/logic/parser/ArgumentMultimap.java index 21e26887a83..b7fbedfff8b 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/spleetwaise/address/logic/parser/ArgumentMultimap.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package spleetwaise.address.logic.parser; import java.util.ArrayList; import java.util.HashMap; @@ -7,24 +7,23 @@ import java.util.Optional; import java.util.stream.Stream; -import seedu.address.logic.Messages; -import seedu.address.logic.parser.exceptions.ParseException; +import spleetwaise.address.logic.Messages; +import spleetwaise.commons.logic.parser.exceptions.ParseException; /** - * Stores mapping of prefixes to their respective arguments. - * Each key may be associated with multiple argument values. - * Values for a given key are stored in a list, and the insertion ordering is maintained. - * Keys are unique, but the list of argument values may contain duplicate argument values, i.e. the same argument value - * can be inserted multiple times for the same prefix. + * Stores mapping of prefixes to their respective arguments. Each key may be associated with multiple argument values. + * Values for a given key are stored in a list, and the insertion ordering is maintained. Keys are unique, but the list + * of argument values may contain duplicate argument values, i.e. the same argument value can be inserted multiple times + * for the same prefix. */ public class ArgumentMultimap { - /** Prefixes mapped to their respective arguments**/ + /** Prefixes mapped to their respective arguments **/ private final Map> argMultimap = new HashMap<>(); /** - * Associates the specified argument value with {@code prefix} key in this map. - * If the map previously contained a mapping for the key, the new value is appended to the list of existing values. + * Associates the specified argument value with {@code prefix} key in this map. If the map previously contained a + * mapping for the key, the new value is appended to the list of existing values. * * @param prefix Prefix key with which the specified argument value is to be associated * @param argValue Argument value to be associated with the specified prefix key @@ -44,9 +43,8 @@ public Optional getValue(Prefix prefix) { } /** - * Returns all values of {@code prefix}. - * If the prefix does not exist or has no values, this will return an empty list. - * Modifying the returned list will not affect the underlying data structure of the ArgumentMultimap. + * Returns all values of {@code prefix}. If the prefix does not exist or has no values, this will return an empty + * list. Modifying the returned list will not affect the underlying data structure of the ArgumentMultimap. */ public List getAllValues(Prefix prefix) { if (!argMultimap.containsKey(prefix)) { @@ -63,8 +61,8 @@ public String getPreamble() { } /** - * Throws a {@code ParseException} if any of the prefixes given in {@code prefixes} appeared more than - * once among the arguments. + * Throws a {@code ParseException} if any of the prefixes given in {@code prefixes} appeared more than once among + * the arguments. */ public void verifyNoDuplicatePrefixesFor(Prefix... prefixes) throws ParseException { Prefix[] duplicatedPrefixes = Stream.of(prefixes).distinct() diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/spleetwaise/address/logic/parser/ArgumentTokenizer.java similarity index 75% rename from src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java rename to src/main/java/spleetwaise/address/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..491e19eea77 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/spleetwaise/address/logic/parser/ArgumentTokenizer.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package spleetwaise.address.logic.parser; import java.util.ArrayList; import java.util.Arrays; @@ -6,12 +6,11 @@ import java.util.stream.Collectors; /** - * Tokenizes arguments string of the form: {@code preamble value value ...}
- * e.g. {@code some preamble text t/ 11.00 t/12.00 k/ m/ July} where prefixes are {@code t/ k/ m/}.
- * 1. An argument's value can be an empty string e.g. the value of {@code k/} in the above example.
- * 2. Leading and trailing whitespaces of an argument value will be discarded.
- * 3. An argument may be repeated and all its values will be accumulated e.g. the value of {@code t/} - * in the above example.
+ * Tokenizes arguments string of the form: {@code preamble value value ...}
e.g. + * {@code some preamble text t/ 11.00 t/12.00 k/ m/ July} where prefixes are {@code t/ k/ m/}.
1. An argument's + * value can be an empty string e.g. the value of {@code k/} in the above example.
2. Leading and trailing + * whitespaces of an argument value will be discarded.
3. An argument may be repeated and all its values will be + * accumulated e.g. the value of {@code t/} in the above example.
*/ public class ArgumentTokenizer { @@ -21,7 +20,7 @@ public class ArgumentTokenizer { * * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixes Prefixes to tokenize the arguments string with - * @return ArgumentMultimap object that maps prefixes to their arguments + * @return ArgumentMultimap object that maps prefixes to their arguments */ public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) { List positions = findAllPrefixPositions(argsString, prefixes); @@ -33,7 +32,7 @@ public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) { * * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixes Prefixes to find in the arguments string - * @return List of zero-based prefix positions in the given arguments string + * @return List of zero-based prefix positions in the given arguments string */ private static List findAllPrefixPositions(String argsString, Prefix... prefixes) { return Arrays.stream(prefixes) @@ -58,16 +57,13 @@ private static List findPrefixPositions(String argsString, Prefi } /** - * Returns the index of the first occurrence of {@code prefix} in - * {@code argsString} starting from index {@code fromIndex}. An occurrence - * is valid if there is a whitespace before {@code prefix}. Returns -1 if no - * such occurrence can be found. + * Returns the index of the first occurrence of {@code prefix} in {@code argsString} starting from index + * {@code fromIndex}. An occurrence is valid if there is a whitespace before {@code prefix}. Returns -1 if no such + * occurrence can be found. * - * E.g if {@code argsString} = "e/hip/900", {@code prefix} = "p/" and - * {@code fromIndex} = 0, this method returns -1 as there are no valid - * occurrences of "p/" with whitespace before it. However, if - * {@code argsString} = "e/hi p/900", {@code prefix} = "p/" and - * {@code fromIndex} = 0, this method returns 5. + * E.g if {@code argsString} = "e/hip/900", {@code prefix} = "p/" and {@code fromIndex} = 0, this method returns -1 + * as there are no valid occurrences of "p/" with whitespace before it. However, if {@code argsString} = "e/hi + * p/900", {@code prefix} = "p/" and {@code fromIndex} = 0, this method returns 5. */ private static int findPrefixPosition(String argsString, String prefix, int fromIndex) { int prefixIndex = argsString.indexOf(" " + prefix, fromIndex); @@ -82,7 +78,7 @@ private static int findPrefixPosition(String argsString, String prefix, int from * * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixPositions Zero-based positions of all prefixes in {@code argsString} - * @return ArgumentMultimap object that maps prefixes to their arguments + * @return ArgumentMultimap object that maps prefixes to their arguments */ private static ArgumentMultimap extractArguments(String argsString, List prefixPositions) { @@ -110,12 +106,14 @@ private static ArgumentMultimap extractArguments(String argsString, List { /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. + * Parses the given {@code String} of arguments in the context of the DeleteCommand and returns a DeleteCommand + * object for execution. + * * @throws ParseException if the user input does not conform the expected format */ public DeleteCommand parse(String args) throws ParseException { @@ -25,5 +27,4 @@ public DeleteCommand parse(String args) throws ParseException { String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); } } - } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/spleetwaise/address/logic/parser/EditCommandParser.java similarity index 59% rename from src/main/java/seedu/address/logic/parser/EditCommandParser.java rename to src/main/java/spleetwaise/address/logic/parser/EditCommandParser.java index 46b3309a78b..128fcbf781c 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/spleetwaise/address/logic/parser/EditCommandParser.java @@ -1,23 +1,25 @@ -package seedu.address.logic.parser; +package spleetwaise.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_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static spleetwaise.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_NAME; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_REMARK; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Collection; import java.util.Collections; import java.util.Optional; import java.util.Set; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; +import spleetwaise.address.logic.commands.EditCommand; +import spleetwaise.address.logic.commands.EditCommand.EditPersonDescriptor; +import spleetwaise.address.model.tag.Tag; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.parser.Parser; +import spleetwaise.commons.logic.parser.exceptions.ParseException; /** * Parses input arguments and creates a new EditCommand object @@ -25,14 +27,15 @@ public class EditCommandParser implements Parser { /** - * Parses the given {@code String} of arguments in the context of the EditCommand - * and returns an EditCommand object for execution. + * Parses the given {@code String} of arguments in the context of the EditCommand and returns an EditCommand object + * for execution. + * * @throws ParseException if the user input does not conform the expected format */ public EditCommand parse(String args) throws ParseException { requireNonNull(args); - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize( + args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_REMARK, PREFIX_TAG); Index index; @@ -42,7 +45,8 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor( + PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_REMARK); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -58,6 +62,9 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } + if (argMultimap.getValue(PREFIX_REMARK).isPresent()) { + editPersonDescriptor.setRemark(ParserUtil.parseRemark(argMultimap.getValue(PREFIX_REMARK).get())); + } parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { @@ -68,9 +75,9 @@ public EditCommand parse(String args) throws ParseException { } /** - * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. - * If {@code tags} contain only one element which is an empty string, it will be parsed into a - * {@code Set} containing zero tags. + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. If {@code tags} + * contain only one element which is an empty string, it will be parsed into a {@code Set} containing zero + * tags. */ private Optional> parseTagsForEdit(Collection tags) throws ParseException { assert tags != null; diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/spleetwaise/address/logic/parser/FindCommandParser.java similarity index 63% rename from src/main/java/seedu/address/logic/parser/FindCommandParser.java rename to src/main/java/spleetwaise/address/logic/parser/FindCommandParser.java index 2867bde857b..b0aaae74424 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/spleetwaise/address/logic/parser/FindCommandParser.java @@ -1,12 +1,13 @@ -package seedu.address.logic.parser; +package spleetwaise.address.logic.parser; -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static spleetwaise.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import java.util.Arrays; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import spleetwaise.address.logic.commands.FindCommand; +import spleetwaise.address.model.person.NameContainsKeywordsPredicate; +import spleetwaise.commons.logic.parser.Parser; +import spleetwaise.commons.logic.parser.exceptions.ParseException; /** * Parses input arguments and creates a new FindCommand object @@ -14,8 +15,9 @@ public class FindCommandParser implements Parser { /** - * Parses the given {@code String} of arguments in the context of the FindCommand - * and returns a FindCommand object for execution. + * Parses the given {@code String} of arguments in the context of the FindCommand and returns a FindCommand object + * for execution. + * * @throws ParseException if the user input does not conform the expected format */ public FindCommand parse(String args) throws ParseException { diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/spleetwaise/address/logic/parser/ParserUtil.java similarity index 61% rename from src/main/java/seedu/address/logic/parser/ParserUtil.java rename to src/main/java/spleetwaise/address/logic/parser/ParserUtil.java index b117acb9c55..e98c23f75ae 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/spleetwaise/address/logic/parser/ParserUtil.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package spleetwaise.address.logic.parser; import static java.util.Objects.requireNonNull; @@ -6,44 +6,28 @@ import java.util.HashSet; import java.util.Set; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import spleetwaise.address.model.person.Address; +import spleetwaise.address.model.person.Email; +import spleetwaise.address.model.person.Name; +import spleetwaise.address.model.person.Phone; +import spleetwaise.address.model.person.Remark; +import spleetwaise.address.model.tag.Tag; +import spleetwaise.commons.logic.parser.BaseParserUtil; +import spleetwaise.commons.logic.parser.exceptions.ParseException; /** * Contains utility methods used for parsing strings in the various *Parser classes. */ -public class ParserUtil { - - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; - - /** - * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be - * trimmed. - * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). - */ - public static Index parseIndex(String oneBasedIndex) throws ParseException { - String trimmedIndex = oneBasedIndex.trim(); - if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { - throw new ParseException(MESSAGE_INVALID_INDEX); - } - return Index.fromOneBased(Integer.parseInt(trimmedIndex)); - } - +public class ParserUtil extends BaseParserUtil { /** - * Parses a {@code String name} into a {@code Name}. - * Leading and trailing whitespaces will be trimmed. + * Parses a {@code String name} into a {@code Name}. Leading and trailing whitespaces will be trimmed. * * @throws ParseException if the given {@code name} is invalid. */ public static Name parseName(String name) throws ParseException { requireNonNull(name); String trimmedName = name.trim(); + if (!Name.isValidName(trimmedName)) { throw new ParseException(Name.MESSAGE_CONSTRAINTS); } @@ -51,8 +35,7 @@ public static Name parseName(String name) throws ParseException { } /** - * Parses a {@code String phone} into a {@code Phone}. - * Leading and trailing whitespaces will be trimmed. + * Parses a {@code String phone} into a {@code Phone}. Leading and trailing whitespaces will be trimmed. * * @throws ParseException if the given {@code phone} is invalid. */ @@ -66,8 +49,7 @@ public static Phone parsePhone(String phone) throws ParseException { } /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. + * Parses a {@code String address} into an {@code Address}. Leading and trailing whitespaces will be trimmed. * * @throws ParseException if the given {@code address} is invalid. */ @@ -81,8 +63,7 @@ public static Address parseAddress(String address) throws ParseException { } /** - * Parses a {@code String email} into an {@code Email}. - * Leading and trailing whitespaces will be trimmed. + * Parses a {@code String email} into an {@code Email}. Leading and trailing whitespaces will be trimmed. * * @throws ParseException if the given {@code email} is invalid. */ @@ -96,8 +77,22 @@ public static Email parseEmail(String email) throws ParseException { } /** - * Parses a {@code String tag} into a {@code Tag}. - * Leading and trailing whitespaces will be trimmed. + * Parses a {@code String remark} into an {@code remark}. Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code remark} is invalid. + */ + public static Remark parseRemark(String remark) throws ParseException { + requireNonNull(remark); + String trimmedRemark = remark.trim(); + + if (!Remark.isValidRemark(trimmedRemark)) { + throw new ParseException(Remark.MESSAGE_CONSTRAINTS); + } + return new Remark(trimmedRemark); + } + + /** + * Parses a {@code String tag} into a {@code Tag}. Leading and trailing whitespaces will be trimmed. * * @throws ParseException if the given {@code tag} is invalid. */ diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/spleetwaise/address/logic/parser/Prefix.java similarity index 89% rename from src/main/java/seedu/address/logic/parser/Prefix.java rename to src/main/java/spleetwaise/address/logic/parser/Prefix.java index 348b7686c8a..3da7bc3813d 100644 --- a/src/main/java/seedu/address/logic/parser/Prefix.java +++ b/src/main/java/spleetwaise/address/logic/parser/Prefix.java @@ -1,8 +1,7 @@ -package seedu.address.logic.parser; +package spleetwaise.address.logic.parser; /** - * A prefix that marks the beginning of an argument in an arguments string. - * E.g. 't/' in 'add James t/ friend'. + * A prefix that marks the beginning of an argument in an arguments string. E.g. 't/' in 'add James t/ friend'. */ public class Prefix { private final String prefix; diff --git a/src/main/java/spleetwaise/address/logic/parser/RemarkCommandParser.java b/src/main/java/spleetwaise/address/logic/parser/RemarkCommandParser.java new file mode 100644 index 00000000000..ecdaaa5e999 --- /dev/null +++ b/src/main/java/spleetwaise/address/logic/parser/RemarkCommandParser.java @@ -0,0 +1,39 @@ +package spleetwaise.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static spleetwaise.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_REMARK; + +import spleetwaise.address.logic.commands.RemarkCommand; +import spleetwaise.address.model.person.Remark; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.exceptions.IllegalValueException; +import spleetwaise.commons.logic.parser.Parser; +import spleetwaise.commons.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code RemarkCommand} object + */ +public class RemarkCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the {@code RemarkCommand} and returns a + * {@code RemarkCommand} object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public RemarkCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_REMARK); + + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemarkCommand.MESSAGE_USAGE), ive); + } + + String remark = argMultimap.getValue(PREFIX_REMARK).orElse(""); + + return new RemarkCommand(index, new Remark(remark)); + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/spleetwaise/address/model/AddressBook.java similarity index 60% rename from src/main/java/seedu/address/model/AddressBook.java rename to src/main/java/spleetwaise/address/model/AddressBook.java index 73397161e84..372352c89cb 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/spleetwaise/address/model/AddressBook.java @@ -1,17 +1,19 @@ -package seedu.address.model; +package spleetwaise.address.model; import static java.util.Objects.requireNonNull; import java.util.List; +import java.util.Optional; import javafx.collections.ObservableList; -import seedu.address.commons.util.ToStringBuilder; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; +import javafx.collections.transformation.FilteredList; +import spleetwaise.address.model.person.Person; +import spleetwaise.address.model.person.Phone; +import spleetwaise.address.model.person.UniquePersonList; +import spleetwaise.commons.util.ToStringBuilder; /** - * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) + * Wraps all data at the address-book level Duplicates are not allowed (by .isSamePerson comparison) */ public class AddressBook implements ReadOnlyAddressBook { @@ -20,7 +22,6 @@ public class AddressBook implements ReadOnlyAddressBook { /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html - * * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication * among constructors. */ @@ -28,7 +29,8 @@ public class AddressBook implements ReadOnlyAddressBook { persons = new UniquePersonList(); } - public AddressBook() {} + public AddressBook() { + } /** * Creates an AddressBook using the Persons in the {@code toBeCopied} @@ -41,8 +43,8 @@ public AddressBook(ReadOnlyAddressBook toBeCopied) { //// list overwrite operations /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. + * Replaces the contents of the person list with {@code persons}. {@code persons} must not contain duplicate + * persons. */ public void setPersons(List persons) { this.persons.setPersons(persons); @@ -59,6 +61,27 @@ public void resetData(ReadOnlyAddressBook newData) { //// person-level operations + /** + * Searches for a person by id and returns an optional Person + */ + public Optional getPersonById(String id) { + requireNonNull(id); + return persons.getPersonById(id); + } + + /** + * Searches for a person by phone and returns an optional Person + */ + public Optional getPersonByPhone(Phone phone) { + requireNonNull(phone); + + FilteredList filteredPersonList = getPersonList().filtered((p) -> p.getPhone().equals(phone)); + if (filteredPersonList.isEmpty()) { + return Optional.empty(); + } + return Optional.of(filteredPersonList.get(0)); + } + /** * Returns true if a person with the same identity as {@code person} exists in the address book. */ @@ -68,17 +91,24 @@ public boolean hasPerson(Person person) { } /** - * Adds a person to the address book. - * The person must not already exist in the address book. + * Returns true if a person with the same identity as {@code person} exists in the address book. + */ + public boolean hasPersonById(Person person) { + requireNonNull(person); + return persons.containsSameId(person); + } + + /** + * Adds a person to the address book. The person must not already exist in the address book. */ public void addPerson(Person p) { persons.add(p); } /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * Replaces the given person {@code target} in the list with {@code editedPerson}. {@code target} must exist in the + * address book. The person identity of {@code editedPerson} must not be the same as another existing person in the + * address book. */ public void setPerson(Person target, Person editedPerson) { requireNonNull(editedPerson); @@ -87,8 +117,7 @@ public void setPerson(Person target, Person editedPerson) { } /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. + * Removes {@code key} from this {@code AddressBook}. {@code key} must exist in the address book. */ public void removePerson(Person key) { persons.remove(key); diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/spleetwaise/address/model/AddressBookModel.java similarity index 50% rename from src/main/java/seedu/address/model/Model.java rename to src/main/java/spleetwaise/address/model/AddressBookModel.java index d54df471c1f..3ce4b1eb4f0 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/spleetwaise/address/model/AddressBookModel.java @@ -1,78 +1,47 @@ -package seedu.address.model; +package spleetwaise.address.model; -import java.nio.file.Path; +import java.util.Optional; import java.util.function.Predicate; import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; +import spleetwaise.address.model.person.Person; +import spleetwaise.address.model.person.Phone; /** - * The API of the Model component. + * The API of the AddressBookModel component. */ -public interface Model { +public interface AddressBookModel { /** {@code Predicate} that always evaluate to true */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; - /** - * Replaces user prefs data with the data in {@code userPrefs}. - */ - void setUserPrefs(ReadOnlyUserPrefs userPrefs); - - /** - * Returns the user prefs. - */ - ReadOnlyUserPrefs getUserPrefs(); - - /** - * Returns the user prefs' GUI settings. - */ - GuiSettings getGuiSettings(); - - /** - * Sets the user prefs' GUI settings. - */ - void setGuiSettings(GuiSettings guiSettings); - - /** - * Returns the user prefs' address book file path. - */ - Path getAddressBookFilePath(); - /** - * Sets the user prefs' address book file path. - */ - void setAddressBookFilePath(Path addressBookFilePath); + /** Returns the AddressBook */ + ReadOnlyAddressBook getAddressBook(); /** * Replaces address book data with the data in {@code addressBook}. */ void setAddressBook(ReadOnlyAddressBook addressBook); - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); - /** * Returns true if a person with the same identity as {@code person} exists in the address book. */ boolean hasPerson(Person person); /** - * Deletes the given person. - * The person must exist in the address book. + * Deletes the given person. The person must exist in the address book. */ void deletePerson(Person target); /** - * Adds the given person. - * {@code person} must not already exist in the address book. + * Adds the given person. {@code person} must not already exist in the address book. */ void addPerson(Person person); /** - * Replaces the given person {@code target} with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * Replaces the given person {@code target} with {@code editedPerson}. {@code target} must exist in the address + * book. The person identity of {@code editedPerson} must not be the same as another existing person in the address + * book. */ void setPerson(Person target, Person editedPerson); @@ -81,7 +50,19 @@ public interface Model { /** * 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); + + + /** + * Searches for a person with the given ID + */ + Optional getPersonById(String id); + + /** + * Searches for a person with the given Phone + */ + Optional getPersonByPhone(Phone phone); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/spleetwaise/address/model/AddressBookModelManager.java similarity index 55% rename from src/main/java/seedu/address/model/ModelManager.java rename to src/main/java/spleetwaise/address/model/AddressBookModelManager.java index 57bc563fde6..74193fa4fe3 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/spleetwaise/address/model/AddressBookModelManager.java @@ -1,92 +1,57 @@ -package seedu.address.model; +package spleetwaise.address.model; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static spleetwaise.commons.util.CollectionUtil.requireAllNonNull; -import java.nio.file.Path; +import java.util.Optional; import java.util.function.Predicate; import java.util.logging.Logger; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; +import spleetwaise.address.model.person.Person; +import spleetwaise.address.model.person.Phone; +import spleetwaise.commons.core.LogsCenter; /** * Represents the in-memory model of the address book data. */ -public class ModelManager implements Model { - private static final Logger logger = LogsCenter.getLogger(ModelManager.class); +public class AddressBookModelManager implements AddressBookModel { + + private static final Logger logger = LogsCenter.getLogger(AddressBookModelManager.class); private final AddressBook addressBook; - private final UserPrefs userPrefs; private final FilteredList filteredPersons; /** - * Initializes a ModelManager with the given addressBook and userPrefs. + * Initializes a AddressBookModelManager with the given addressBook and userPrefs. */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { - requireAllNonNull(addressBook, userPrefs); + public AddressBookModelManager(ReadOnlyAddressBook addressBook) { + requireAllNonNull(addressBook); - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); + logger.fine( + "Initializing AddressBook Model with address book: " + addressBook); this.addressBook = new AddressBook(addressBook); - this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); } - public ModelManager() { - this(new AddressBook(), new UserPrefs()); - } - - //=========== UserPrefs ================================================================================== - - @Override - public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { - requireNonNull(userPrefs); - this.userPrefs.resetData(userPrefs); - } - - @Override - public ReadOnlyUserPrefs getUserPrefs() { - return userPrefs; - } - - @Override - public GuiSettings getGuiSettings() { - return userPrefs.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - requireNonNull(guiSettings); - userPrefs.setGuiSettings(guiSettings); + public AddressBookModelManager() { + this(new AddressBook()); } - @Override - public Path getAddressBookFilePath() { - return userPrefs.getAddressBookFilePath(); - } + //=========== AddressBook ================================================================================ @Override - public void setAddressBookFilePath(Path addressBookFilePath) { - requireNonNull(addressBookFilePath); - userPrefs.setAddressBookFilePath(addressBookFilePath); + public ReadOnlyAddressBook getAddressBook() { + return addressBook; } - //=========== AddressBook ================================================================================ - @Override public void setAddressBook(ReadOnlyAddressBook addressBook) { this.addressBook.resetData(addressBook); } - @Override - public ReadOnlyAddressBook getAddressBook() { - return addressBook; - } - @Override public boolean hasPerson(Person person) { requireNonNull(person); @@ -95,6 +60,7 @@ public boolean hasPerson(Person person) { @Override public void deletePerson(Person target) { + requireNonNull(target); addressBook.removePerson(target); } @@ -128,6 +94,18 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } + @Override + public Optional getPersonById(String id) { + requireNonNull(id); + return addressBook.getPersonById(id); + } + + @Override + public Optional getPersonByPhone(Phone phone) { + requireNonNull(phone); + return addressBook.getPersonByPhone(phone); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -135,13 +113,12 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof ModelManager)) { + if (!(other instanceof AddressBookModelManager)) { return false; } - ModelManager otherModelManager = (ModelManager) other; + AddressBookModelManager otherModelManager = (AddressBookModelManager) other; return addressBook.equals(otherModelManager.addressBook) - && userPrefs.equals(otherModelManager.userPrefs) && filteredPersons.equals(otherModelManager.filteredPersons); } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/spleetwaise/address/model/ReadOnlyAddressBook.java similarity index 50% rename from src/main/java/seedu/address/model/ReadOnlyAddressBook.java rename to src/main/java/spleetwaise/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..787b4ba2546 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/spleetwaise/address/model/ReadOnlyAddressBook.java @@ -1,7 +1,7 @@ -package seedu.address.model; +package spleetwaise.address.model; import javafx.collections.ObservableList; -import seedu.address.model.person.Person; +import spleetwaise.address.model.person.Person; /** * Unmodifiable view of an address book @@ -9,8 +9,7 @@ public interface ReadOnlyAddressBook { /** - * Returns an unmodifiable view of the persons list. - * This list will not contain any duplicate persons. + * Returns an unmodifiable view of the persons list. This list will not contain any duplicate persons. */ ObservableList getPersonList(); diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/spleetwaise/address/model/person/Address.java similarity index 70% rename from src/main/java/seedu/address/model/person/Address.java rename to src/main/java/spleetwaise/address/model/person/Address.java index 469a2cc9a1e..83c01b0bd65 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/spleetwaise/address/model/person/Address.java @@ -1,11 +1,12 @@ -package seedu.address.model.person; +package spleetwaise.address.model.person; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; + +import spleetwaise.commons.util.AppUtil; /** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} + * Represents a Person's address in the address book. Guarantees: immutable; is valid as declared in + * {@link #isValidAddress(String)} */ public class Address { @@ -26,15 +27,16 @@ public class Address { */ public Address(String address) { requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; + String trimmedAddress = address.trim(); + AppUtil.checkArgument(isValidAddress(trimmedAddress), MESSAGE_CONSTRAINTS); + value = trimmedAddress; } /** - * Returns true if a given string is a valid email. + * Returns true if a given string is a valid address */ public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); + return test.trim().matches(VALIDATION_REGEX); } @Override diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/spleetwaise/address/model/person/Email.java similarity index 90% rename from src/main/java/seedu/address/model/person/Email.java rename to src/main/java/spleetwaise/address/model/person/Email.java index c62e512bc29..0edec6adf3e 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/spleetwaise/address/model/person/Email.java @@ -1,11 +1,12 @@ -package seedu.address.model.person; +package spleetwaise.address.model.person; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; + +import spleetwaise.commons.util.AppUtil; /** - * Represents a Person's email in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} + * Represents a Person's email in the address book. Guarantees: immutable; is valid as declared in + * {@link #isValidEmail(String)} */ public class Email { @@ -40,7 +41,7 @@ public class Email { */ public Email(String email) { requireNonNull(email); - checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); + AppUtil.checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); value = email; } diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/spleetwaise/address/model/person/Name.java similarity index 59% rename from src/main/java/seedu/address/model/person/Name.java rename to src/main/java/spleetwaise/address/model/person/Name.java index 173f15b9b00..0544e585348 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/spleetwaise/address/model/person/Name.java @@ -1,22 +1,24 @@ -package seedu.address.model.person; +package spleetwaise.address.model.person; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; + +import spleetwaise.commons.util.AppUtil; /** - * Represents a Person's name in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} + * Represents a Person's name in the address book. Guarantees: immutable; is valid as declared in + * {@link #isValidName(String)} */ public class Name { public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + "Names should only contain alphanumeric characters and spaces, and certain special characters " + + "(.'&()\"/-) that are allowed as legal names or for ease of record and it should not be blank"; /* - * The first character of the address must not be a whitespace, + * The first character of the name must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "[A-Za-z0-9.'&(),\\-][A-Za-z0-9.'&(),\"\\-/ ]*"; public final String fullName; @@ -27,15 +29,16 @@ public class Name { */ public Name(String name) { requireNonNull(name); - checkArgument(isValidName(name), MESSAGE_CONSTRAINTS); - fullName = name; + String trimmedName = name.trim(); + AppUtil.checkArgument(isValidName(trimmedName), MESSAGE_CONSTRAINTS); + fullName = trimmedName; } /** * Returns true if a given string is a valid name. */ public static boolean isValidName(String test) { - return test.matches(VALIDATION_REGEX); + return test.trim().matches(VALIDATION_REGEX); } diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/spleetwaise/address/model/person/NameContainsKeywordsPredicate.java similarity index 89% rename from src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java rename to src/main/java/spleetwaise/address/model/person/NameContainsKeywordsPredicate.java index 62d19be2977..97fb512fbd7 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/spleetwaise/address/model/person/NameContainsKeywordsPredicate.java @@ -1,10 +1,10 @@ -package seedu.address.model.person; +package spleetwaise.address.model.person; import java.util.List; import java.util.function.Predicate; -import seedu.address.commons.util.StringUtil; -import seedu.address.commons.util.ToStringBuilder; +import spleetwaise.commons.util.StringUtil; +import spleetwaise.commons.util.ToStringBuilder; /** * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/spleetwaise/address/model/person/Person.java similarity index 52% rename from src/main/java/seedu/address/model/person/Person.java rename to src/main/java/spleetwaise/address/model/person/Person.java index abe8c46b535..2c311378d98 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/spleetwaise/address/model/person/Person.java @@ -1,25 +1,26 @@ -package seedu.address.model.person; - -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +package spleetwaise.address.model.person; import java.util.Collections; import java.util.HashSet; -import java.util.Objects; import java.util.Set; -import seedu.address.commons.util.ToStringBuilder; -import seedu.address.model.tag.Tag; +import spleetwaise.address.model.tag.Tag; +import spleetwaise.commons.util.CollectionUtil; +import spleetwaise.commons.util.IdUtil; +import spleetwaise.commons.util.ToStringBuilder; /** - * Represents a Person in the address book. - * Guarantees: details are present and not null, field values are validated, immutable. + * Represents a Person in the address book. Guarantees: details are present and not null, field values are validated, + * immutable. */ public class Person { // Identity fields + private final String id; private final Name name; private final Phone phone; private final Email email; + private final Remark remark; // Data fields private final Address address; @@ -28,15 +29,25 @@ public class Person { /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(String id, Name name, Phone phone, Email email, Address address, Remark remark, Set tags) { + CollectionUtil.requireAllNonNull(name, phone, email, address, remark, tags); + this.id = id; this.name = name; this.phone = phone; this.email = email; this.address = address; + this.remark = remark; this.tags.addAll(tags); } + public Person(Name name, Phone phone, Email email, Address address, Remark remark, Set tags) { + this(IdUtil.getId(), name, phone, email, address, remark, tags); + } + + public String getId() { + return id; + } + public Name getName() { return name; } @@ -53,30 +64,40 @@ public Address getAddress() { return address; } + public Remark getRemark() { + return remark; + } + /** - * Returns an immutable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. + * Returns an immutable tag set, which throws {@code UnsupportedOperationException} if modification is attempted. */ public Set getTags() { return Collections.unmodifiableSet(tags); } /** - * Returns true if both persons have the same name. - * This defines a weaker notion of equality between two persons. + * Returns true if both persons have the same name and phone number. This defines a notion of equality between two + * persons based on their identity. */ public boolean isSamePerson(Person otherPerson) { if (otherPerson == this) { + // This means that the otherPerson has the same id, name, phone, email, address, remark and tags return true; } - return otherPerson != null - && otherPerson.getName().equals(getName()); + return otherPerson != null && name.equals(otherPerson.name) && phone.equals(otherPerson.phone); + } + + /** + * Returns true if both persons have the same id. + */ + public boolean hasSameId(Person otherPerson) { + return otherPerson.getId().equals(getId()); } /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. + * Returns true if both persons have the same identity and data fields. This defines a stronger notion of equality + * between two persons. */ @Override public boolean equals(Object other) { @@ -85,22 +106,22 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof Person)) { + if (!(other instanceof Person otherPerson)) { return false; } - Person otherPerson = (Person) other; - return name.equals(otherPerson.name) + return id.equals(otherPerson.id) + && name.equals(otherPerson.name) && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) + && remark.equals(otherPerson.remark) && tags.equals(otherPerson.tags); } @Override public int hashCode() { - // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return this.id.hashCode(); } @Override @@ -110,6 +131,7 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("remark", remark) .add("tags", tags) .toString(); } diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/spleetwaise/address/model/person/Phone.java similarity index 79% rename from src/main/java/seedu/address/model/person/Phone.java rename to src/main/java/spleetwaise/address/model/person/Phone.java index d733f63d739..11786622fad 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/spleetwaise/address/model/person/Phone.java @@ -1,11 +1,12 @@ -package seedu.address.model.person; +package spleetwaise.address.model.person; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; + +import spleetwaise.commons.util.AppUtil; /** - * Represents a Person's phone number in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} + * Represents a Person's phone number in the address book. Guarantees: immutable; is valid as declared in + * {@link #isValidPhone(String)} */ public class Phone { @@ -22,7 +23,7 @@ public class Phone { */ public Phone(String phone) { requireNonNull(phone); - checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); + AppUtil.checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); value = phone; } diff --git a/src/main/java/spleetwaise/address/model/person/Remark.java b/src/main/java/spleetwaise/address/model/person/Remark.java new file mode 100644 index 00000000000..7fc8d030ecf --- /dev/null +++ b/src/main/java/spleetwaise/address/model/person/Remark.java @@ -0,0 +1,50 @@ +package spleetwaise.address.model.person; + +import static java.util.Objects.requireNonNull; + +import spleetwaise.commons.util.AppUtil; + +/** + * Represents a Person's remark in the address book. Guarantees: immutable; is valid as declared in + * {@link #isValidRemark(String)} + */ +public class Remark { + public static final int MAX_LENGTH = 120; + public static final String MESSAGE_CONSTRAINTS = "Remarks should not be blank or more than " + MAX_LENGTH + + " characters."; + public final String value; + + /** + * Constructor for remark + */ + public Remark(String remark) { + requireNonNull(remark); + String trimmedRemark = remark.trim(); + AppUtil.checkArgument(isValidRemark(trimmedRemark), MESSAGE_CONSTRAINTS); + value = trimmedRemark; + } + + /** + * Returns true if a given string is a valid remark (allows empty string ""). + */ + public static boolean isValidRemark(String test) { + return test.trim().length() <= MAX_LENGTH; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Remark // instanceof handles nulls + && value.equals(((Remark) other).value)); // state check + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/spleetwaise/address/model/person/UniquePersonList.java similarity index 67% rename from src/main/java/seedu/address/model/person/UniquePersonList.java rename to src/main/java/spleetwaise/address/model/person/UniquePersonList.java index cc0a68d79f9..95e1b7c0105 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/spleetwaise/address/model/person/UniquePersonList.java @@ -1,22 +1,23 @@ -package seedu.address.model.person; +package spleetwaise.address.model.person; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.util.Iterator; import java.util.List; +import java.util.Optional; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import seedu.address.model.person.exceptions.DuplicatePersonException; -import seedu.address.model.person.exceptions.PersonNotFoundException; +import spleetwaise.address.model.person.exceptions.DuplicatePersonException; +import spleetwaise.address.model.person.exceptions.PersonNotFoundException; +import spleetwaise.commons.util.CollectionUtil; /** - * A list of persons that enforces uniqueness between its elements and does not allow nulls. - * A person is considered unique by comparing using {@code Person#isSamePerson(Person)}. As such, adding and updating of - * persons uses Person#isSamePerson(Person) for equality so as to ensure that the person being added or updated is - * unique in terms of identity in the UniquePersonList. However, the removal of a person uses Person#equals(Object) so - * as to ensure that the person with exactly the same fields will be removed. + * A list of persons that enforces uniqueness between its elements and does not allow nulls. A person is considered + * unique by comparing using {@code Person#isSamePerson(Person)}. As such, adding and updating of persons uses + * Person#isSamePerson(Person) for equality so as to ensure that the person being added or updated is unique in terms of + * identity in the UniquePersonList. However, the removal of a person uses Person#equals(Object) so as to ensure that + * the person with exactly the same fields will be removed. * * Supports a minimal set of list operations. * @@ -37,8 +38,25 @@ public boolean contains(Person toCheck) { } /** - * Adds a person to the list. - * The person must not already exist in the list. + * Returns true if the list contains an equivalent person as the given argument. + */ + public boolean containsSameId(Person toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::hasSameId); + } + + /** + * Searches for a person by id and returns an optional Person + */ + public Optional getPersonById(String id) { + requireNonNull(id); + return internalList.stream() + .filter(person -> id.equals(person.getId())) + .findFirst(); + } + + /** + * Adds a person to the list. The person must not already exist in the list. */ public void add(Person toAdd) { requireNonNull(toAdd); @@ -49,12 +67,11 @@ public void add(Person toAdd) { } /** - * Replaces the person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the list. + * Replaces the person {@code target} in the list with {@code editedPerson}. {@code target} must exist in the list. * The person identity of {@code editedPerson} must not be the same as another existing person in the list. */ public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); + CollectionUtil.requireAllNonNull(target, editedPerson); int index = internalList.indexOf(target); if (index == -1) { @@ -69,8 +86,7 @@ public void setPerson(Person target, Person editedPerson) { } /** - * Removes the equivalent person from the list. - * The person must exist in the list. + * Removes the equivalent person from the list. The person must exist in the list. */ public void remove(Person toRemove) { requireNonNull(toRemove); @@ -85,11 +101,10 @@ public void setPersons(UniquePersonList replacement) { } /** - * Replaces the contents of this list with {@code persons}. - * {@code persons} must not contain duplicate persons. + * Replaces the contents of this list with {@code persons}. {@code persons} must not contain duplicate persons. */ public void setPersons(List persons) { - requireAllNonNull(persons); + CollectionUtil.requireAllNonNull(persons); if (!personsAreUnique(persons)) { throw new DuplicatePersonException(); } diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/spleetwaise/address/model/person/exceptions/DuplicatePersonException.java similarity index 85% rename from src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java rename to src/main/java/spleetwaise/address/model/person/exceptions/DuplicatePersonException.java index d7290f59442..f34dd9ee4bf 100644 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ b/src/main/java/spleetwaise/address/model/person/exceptions/DuplicatePersonException.java @@ -1,4 +1,4 @@ -package seedu.address.model.person.exceptions; +package spleetwaise.address.model.person.exceptions; /** * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same diff --git a/src/main/java/spleetwaise/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/spleetwaise/address/model/person/exceptions/PersonNotFoundException.java new file mode 100644 index 00000000000..e3d24163b44 --- /dev/null +++ b/src/main/java/spleetwaise/address/model/person/exceptions/PersonNotFoundException.java @@ -0,0 +1,7 @@ +package spleetwaise.address.model.person.exceptions; + +/** + * Signals that the operation is unable to find the specified person. + */ +public class PersonNotFoundException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/spleetwaise/address/model/tag/Tag.java similarity index 72% rename from src/main/java/seedu/address/model/tag/Tag.java rename to src/main/java/spleetwaise/address/model/tag/Tag.java index f1a0d4e233b..e568f9387ed 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/spleetwaise/address/model/tag/Tag.java @@ -1,11 +1,12 @@ -package seedu.address.model.tag; +package spleetwaise.address.model.tag; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; + +import spleetwaise.commons.util.AppUtil; /** - * Represents a Tag in the address book. - * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} + * Represents a Tag in the address book. Guarantees: immutable; name is valid as declared in + * {@link #isValidTagName(String)} */ public class Tag { @@ -21,15 +22,16 @@ public class Tag { */ public Tag(String tagName) { requireNonNull(tagName); - checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); - this.tagName = tagName; + String trimmedTagName = tagName.trim(); + AppUtil.checkArgument(isValidTagName(trimmedTagName), MESSAGE_CONSTRAINTS); + this.tagName = trimmedTagName; } /** * Returns true if a given string is a valid tag name. */ public static boolean isValidTagName(String test) { - return test.matches(VALIDATION_REGEX); + return test.trim().matches(VALIDATION_REGEX); } @Override diff --git a/src/main/java/spleetwaise/address/model/util/SampleDataUtil.java b/src/main/java/spleetwaise/address/model/util/SampleDataUtil.java new file mode 100644 index 00000000000..900c3563b97 --- /dev/null +++ b/src/main/java/spleetwaise/address/model/util/SampleDataUtil.java @@ -0,0 +1,109 @@ +package spleetwaise.address.model.util; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import spleetwaise.address.model.AddressBook; +import spleetwaise.address.model.ReadOnlyAddressBook; +import spleetwaise.address.model.person.Address; +import spleetwaise.address.model.person.Email; +import spleetwaise.address.model.person.Name; +import spleetwaise.address.model.person.Person; +import spleetwaise.address.model.person.Phone; +import spleetwaise.address.model.person.Remark; +import spleetwaise.address.model.tag.Tag; +import spleetwaise.transaction.model.ReadOnlyTransactionBook; +import spleetwaise.transaction.model.TransactionBook; +import spleetwaise.transaction.model.transaction.Amount; +import spleetwaise.transaction.model.transaction.Category; +import spleetwaise.transaction.model.transaction.Date; +import spleetwaise.transaction.model.transaction.Description; +import spleetwaise.transaction.model.transaction.Status; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Contains utility methods for populating {@code AddressBook} with sample data. + */ +public class SampleDataUtil { + public static final Remark EMPTY_REMARK = new Remark(""); + + private static final Person alexYeoh = new Person(new Name("Alex Yeoh"), new Phone("87438807"), + new Email("alexyeoh@example.com"), + new Address("Blk 30 Geylang Street 29, #06-40"), EMPTY_REMARK, getTagSet("friends") + ); + private static final Person berniceYu = new Person(new Name("Bernice Yu"), new Phone("99272758"), + new Email("berniceyu" + + "@example.com"), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), EMPTY_REMARK, + getTagSet("colleagues", "friends") + ); + private static final Person charlotteOliveiro = new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), + new Email("charlotte@example.com"), + new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), EMPTY_REMARK, getTagSet("neighbours") + ); + private static final Person davidLi = new Person(new Name("David Li"), new Phone("91031282"), + new Email("lidavid@example.com"), + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), EMPTY_REMARK, getTagSet("family") + ); + private static final Person irfanIbrahim = new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), + new Email("irfan@example.com"), + new Address("Blk 47 Tampines Street 20, #17-35"), EMPTY_REMARK, getTagSet("classmates") + ); + private static final Person royBalakrishnan = new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), + new Email("royb@example.com"), + new Address("Blk 45 Aljunied Street 85, #11-31"), EMPTY_REMARK, getTagSet("colleagues") + ); + + public static Person[] getSamplePersons() { + return new Person[]{ alexYeoh, berniceYu, charlotteOliveiro, davidLi, irfanIbrahim, royBalakrishnan }; + } + + public static Transaction[] getSampleTransactions() { + Set emptyCategories = new HashSet<>(); + return new Transaction[]{ new Transaction(alexYeoh, new Amount("10.00"), new Description("Mc Donald's"), + new Date("01012024"), emptyCategories, new Status("Not Done") + ), new Transaction( + berniceYu, new Amount("5.50"), new Description("Starbucks"), new Date("02022024"), emptyCategories, + new Status("Not Done") + ), new Transaction( + charlotteOliveiro, new Amount("8.25"), new Description("Pizza Hut"), new Date("03032024"), + emptyCategories, new Status("Not Done") + ), new Transaction( + davidLi, new Amount("12.00"), new Description("NTUC FairPrice"), new Date("04042024"), emptyCategories, + new Status("Not Done") + ), new Transaction( + irfanIbrahim, new Amount("-9.50"), new Description("Cold Storage"), new Date("05052024"), + emptyCategories, new Status("Not Done") + ), new Transaction( + royBalakrishnan, new Amount("11.25"), new Description("Old Chang Kee"), new Date("06062024"), + emptyCategories, new Status("Not Done") + ) + }; + } + + public static ReadOnlyAddressBook getSampleAddressBook() { + AddressBook sampleAb = new AddressBook(); + for (Person samplePerson : getSamplePersons()) { + sampleAb.addPerson(samplePerson); + } + return sampleAb; + } + + public static ReadOnlyTransactionBook getSampleTransactionBook() { + TransactionBook sampleTb = new TransactionBook(); + for (Transaction t : getSampleTransactions()) { + sampleTb.addTransaction(t); + } + return sampleTb; + } + + /** + * Returns a tag set containing the list of strings given. + */ + public static Set getTagSet(String... strings) { + return Arrays.stream(strings).map(Tag::new).collect(Collectors.toSet()); + } + +} diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/spleetwaise/address/storage/AddressBookStorage.java similarity index 76% rename from src/main/java/seedu/address/storage/AddressBookStorage.java rename to src/main/java/spleetwaise/address/storage/AddressBookStorage.java index f2e015105ae..e8134fe01b8 100644 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ b/src/main/java/spleetwaise/address/storage/AddressBookStorage.java @@ -1,14 +1,15 @@ -package seedu.address.storage; +package spleetwaise.address.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.model.ReadOnlyAddressBook; +import spleetwaise.address.model.AddressBook; +import spleetwaise.address.model.ReadOnlyAddressBook; +import spleetwaise.commons.exceptions.DataLoadingException; /** - * Represents a storage for {@link seedu.address.model.AddressBook}. + * Represents a storage for {@link AddressBook}. */ public interface AddressBookStorage { @@ -18,8 +19,8 @@ public interface AddressBookStorage { Path getAddressBookFilePath(); /** - * Returns AddressBook data as a {@link ReadOnlyAddressBook}. - * Returns {@code Optional.empty()} if storage file is not found. + * Returns AddressBook data as a {@link ReadOnlyAddressBook}. Returns {@code Optional.empty()} if storage file is + * not found. * * @throws DataLoadingException if loading the data from storage failed. */ @@ -32,6 +33,7 @@ public interface AddressBookStorage { /** * Saves the given {@link ReadOnlyAddressBook} to the storage. + * * @param addressBook cannot be null. * @throws IOException if there was any problem writing to the file. */ diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/spleetwaise/address/storage/JsonAdaptedPerson.java similarity index 64% rename from src/main/java/seedu/address/storage/JsonAdaptedPerson.java rename to src/main/java/spleetwaise/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..501457e24ed 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/spleetwaise/address/storage/JsonAdaptedPerson.java @@ -1,4 +1,4 @@ -package seedu.address.storage; +package spleetwaise.address.storage; import java.util.ArrayList; import java.util.HashSet; @@ -9,38 +9,46 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import spleetwaise.address.model.person.Address; +import spleetwaise.address.model.person.Email; +import spleetwaise.address.model.person.Name; +import spleetwaise.address.model.person.Person; +import spleetwaise.address.model.person.Phone; +import spleetwaise.address.model.person.Remark; +import spleetwaise.address.model.tag.Tag; +import spleetwaise.commons.exceptions.IllegalValueException; +import spleetwaise.commons.util.IdUtil; /** * Jackson-friendly version of {@link Person}. */ -class JsonAdaptedPerson { +public class JsonAdaptedPerson { public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; + private final String id; private final String name; private final String phone; private final String email; private final String address; + private final String remark; private final List tags = new ArrayList<>(); /** * Constructs a {@code JsonAdaptedPerson} with the given person details. */ @JsonCreator - public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, + public JsonAdaptedPerson( + @JsonProperty("id") String id, @JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("remark") String remark, @JsonProperty("tags") List tags + ) { + this.id = id; this.name = name; this.phone = phone; this.email = email; this.address = address; + this.remark = remark; if (tags != null) { this.tags.addAll(tags); } @@ -50,13 +58,13 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone * Converts a given {@code Person} into this class for Jackson use. */ public JsonAdaptedPerson(Person source) { + id = source.getId(); name = source.getName().fullName; phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; - tags.addAll(source.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList())); + remark = source.getRemark().value; + tags.addAll(source.getTags().stream().map(JsonAdaptedTag::new).collect(Collectors.toList())); } /** @@ -70,6 +78,9 @@ public Person toModelType() throws IllegalValueException { personTags.add(tag.toModelType()); } + if (id == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "id")); + } if (name == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); } @@ -102,8 +113,20 @@ public Person toModelType() throws IllegalValueException { } final Address modelAddress = new Address(address); + if (remark == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Remark.class.getSimpleName())); + } + if (!Remark.isValidRemark(remark)) { + throw new IllegalValueException(Remark.MESSAGE_CONSTRAINTS); + } + final Remark modelRemark = new Remark(remark); + final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } + if (!IdUtil.isValidId(id)) { + throw new IllegalValueException(IdUtil.MESSAGE_CONSTRAINTS); + } + + return new Person(id.trim(), modelName, modelPhone, modelEmail, modelAddress, modelRemark, modelTags); + } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/spleetwaise/address/storage/JsonAdaptedTag.java similarity index 86% rename from src/main/java/seedu/address/storage/JsonAdaptedTag.java rename to src/main/java/spleetwaise/address/storage/JsonAdaptedTag.java index 0df22bdb754..7d4f3dc9e6d 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ b/src/main/java/spleetwaise/address/storage/JsonAdaptedTag.java @@ -1,15 +1,15 @@ -package seedu.address.storage; +package spleetwaise.address.storage; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; +import spleetwaise.address.model.tag.Tag; +import spleetwaise.commons.exceptions.IllegalValueException; /** * Jackson-friendly version of {@link Tag}. */ -class JsonAdaptedTag { +public class JsonAdaptedTag { private final String tagName; diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/spleetwaise/address/storage/JsonAddressBookStorage.java similarity index 86% rename from src/main/java/seedu/address/storage/JsonAddressBookStorage.java rename to src/main/java/spleetwaise/address/storage/JsonAddressBookStorage.java index 41e06f264e1..6cfbb1e5cc2 100644 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ b/src/main/java/spleetwaise/address/storage/JsonAddressBookStorage.java @@ -1,4 +1,4 @@ -package seedu.address.storage; +package spleetwaise.address.storage; import static java.util.Objects.requireNonNull; @@ -7,12 +7,12 @@ import java.util.Optional; import java.util.logging.Logger; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.commons.util.FileUtil; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyAddressBook; +import spleetwaise.address.model.ReadOnlyAddressBook; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.exceptions.DataLoadingException; +import spleetwaise.commons.exceptions.IllegalValueException; +import spleetwaise.commons.util.FileUtil; +import spleetwaise.commons.util.JsonUtil; /** * A class to access AddressBook data stored as a json file on the hard disk. diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/spleetwaise/address/storage/JsonSerializableAddressBook.java similarity index 58% rename from src/main/java/seedu/address/storage/JsonSerializableAddressBook.java rename to src/main/java/spleetwaise/address/storage/JsonSerializableAddressBook.java index 5efd834091d..f5b1741f940 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/spleetwaise/address/storage/JsonSerializableAddressBook.java @@ -1,17 +1,19 @@ -package seedu.address.storage; +package spleetwaise.address.storage; import java.util.ArrayList; import java.util.List; +import java.util.logging.Logger; import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonRootName; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; +import spleetwaise.address.model.AddressBook; +import spleetwaise.address.model.ReadOnlyAddressBook; +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.exceptions.IllegalValueException; /** * An Immutable AddressBook that is serializable to JSON format. @@ -20,6 +22,9 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_ID = "Persons list contains duplicate ids."; + + private static final Logger logger = LogsCenter.getLogger(JsonSerializableAddressBook.class); private final List persons = new ArrayList<>(); @@ -48,13 +53,23 @@ public JsonSerializableAddressBook(ReadOnlyAddressBook source) { public AddressBook toModelType() throws IllegalValueException { AddressBook addressBook = new AddressBook(); for (JsonAdaptedPerson jsonAdaptedPerson : persons) { - Person person = jsonAdaptedPerson.toModelType(); - if (addressBook.hasPerson(person)) { - throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); + try { + Person person = jsonAdaptedPerson.toModelType(); + + if (addressBook.hasPerson(person)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); + } else if (addressBook.hasPersonById(person)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_ID); + } + + addressBook.addPerson(person); + } catch (IllegalValueException e) { + logger.warning(String.format( + "Address book is possibly corrupted: %s Ignoring corrupted person.", + e.getMessage() + )); } - addressBook.addPerson(person); } return addressBook; } - } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/spleetwaise/address/ui/PersonCard.java similarity index 79% rename from src/main/java/seedu/address/ui/PersonCard.java rename to src/main/java/spleetwaise/address/ui/PersonCard.java index 094c42cda82..33c6cca27f3 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/spleetwaise/address/ui/PersonCard.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package spleetwaise.address.ui; import java.util.Comparator; @@ -7,7 +7,8 @@ import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; -import seedu.address.model.person.Person; +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.ui.UiPart; /** * An UI component that displays information of a {@code Person}. @@ -17,9 +18,8 @@ public class PersonCard extends UiPart { private static final String FXML = "PersonListCard.fxml"; /** - * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. - * As a consequence, UI elements' variable names cannot be set to such keywords - * or an exception will be thrown by JavaFX during runtime. + * 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 */ @@ -39,6 +39,8 @@ public class PersonCard extends UiPart { @FXML private Label email; @FXML + private Label remark; + @FXML private FlowPane tags; /** @@ -52,6 +54,7 @@ public PersonCard(Person person, int displayedIndex) { phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); + remark.setText(person.getRemark().value); person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/spleetwaise/address/ui/PersonListPanel.java similarity index 89% rename from src/main/java/seedu/address/ui/PersonListPanel.java rename to src/main/java/spleetwaise/address/ui/PersonListPanel.java index f4c501a897b..1af9fc62a9e 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/spleetwaise/address/ui/PersonListPanel.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package spleetwaise.address.ui; import java.util.logging.Logger; @@ -7,8 +7,9 @@ import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.layout.Region; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.ui.UiPart; /** * Panel containing the list of persons. diff --git a/src/main/java/seedu/address/AppParameters.java b/src/main/java/spleetwaise/commons/AppParameters.java similarity index 86% rename from src/main/java/seedu/address/AppParameters.java rename to src/main/java/spleetwaise/commons/AppParameters.java index 3d603622d4e..b9163f1a8c8 100644 --- a/src/main/java/seedu/address/AppParameters.java +++ b/src/main/java/spleetwaise/commons/AppParameters.java @@ -1,15 +1,16 @@ -package seedu.address; +package spleetwaise.commons; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.logging.Logger; import javafx.application.Application; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.FileUtil; -import seedu.address.commons.util.ToStringBuilder; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.util.FileUtil; +import spleetwaise.commons.util.ToStringBuilder; /** * Represents the parsed command-line parameters given to the application. @@ -19,20 +20,12 @@ public class AppParameters { private Path configPath; - public Path getConfigPath() { - return configPath; - } - - public void setConfigPath(Path configPath) { - this.configPath = configPath; - } - /** * Parses the application command-line parameters. */ public static AppParameters parse(Application.Parameters parameters) { AppParameters appParameters = new AppParameters(); - Map namedParameters = parameters.getNamed(); + Map namedParameters = parameters != null ? parameters.getNamed() : new HashMap<>(); String configPathParameter = namedParameters.get("config"); if (configPathParameter != null && !FileUtil.isValidPath(configPathParameter)) { @@ -44,6 +37,14 @@ public static AppParameters parse(Application.Parameters parameters) { return appParameters; } + public Path getConfigPath() { + return configPath; + } + + public void setConfigPath(Path configPath) { + this.configPath = configPath; + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/spleetwaise/commons/Main.java similarity index 71% rename from src/main/java/seedu/address/Main.java rename to src/main/java/spleetwaise/commons/Main.java index 9461d6da769..2a019b8a9a3 100644 --- a/src/main/java/seedu/address/Main.java +++ b/src/main/java/spleetwaise/commons/Main.java @@ -1,25 +1,23 @@ -package seedu.address; +package spleetwaise.commons; import java.util.logging.Logger; import javafx.application.Application; -import seedu.address.commons.core.LogsCenter; +import spleetwaise.commons.core.LogsCenter; /** * The main entry point to the application. * - * This is a workaround for the following error when MainApp is made the - * entry point of the application: + * This is a workaround for the following error when MainApp is made the entry point of the application: * - * Error: JavaFX runtime components are missing, and are required to run this application + * Error: JavaFX runtime components are missing, and are required to run this application * - * The reason is that MainApp extends Application. In that case, the - * LauncherHelper will check for the javafx.graphics module to be present - * as a named module. We don't use JavaFX via the module system so it can't - * find the javafx.graphics module, and so the launch is aborted. + * The reason is that MainApp extends Application. In that case, the LauncherHelper will check for the javafx.graphics + * module to be present as a named module. We don't use JavaFX via the module system so it can't find the + * javafx.graphics module, and so the launch is aborted. * - * By having a separate main class (Main) that doesn't extend Application - * to be the entry point of the application, we avoid this issue. + * By having a separate main class (Main) that doesn't extend Application to be the entry point of the application, we + * avoid this issue. */ public class Main { private static Logger logger = LogsCenter.getLogger(Main.class); diff --git a/src/main/java/spleetwaise/commons/MainApp.java b/src/main/java/spleetwaise/commons/MainApp.java new file mode 100644 index 00000000000..7a62e978420 --- /dev/null +++ b/src/main/java/spleetwaise/commons/MainApp.java @@ -0,0 +1,229 @@ +package spleetwaise.commons; + +import static spleetwaise.commons.logic.LogicManager.FILE_OPS_ERROR_FORMAT; +import static spleetwaise.commons.logic.LogicManager.FILE_OPS_PERMISSION_ERROR_FORMAT; + +import java.io.IOException; +import java.nio.file.AccessDeniedException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import javafx.application.Application; +import javafx.stage.Stage; +import spleetwaise.address.model.AddressBook; +import spleetwaise.address.model.AddressBookModel; +import spleetwaise.address.model.AddressBookModelManager; +import spleetwaise.address.model.ReadOnlyAddressBook; +import spleetwaise.address.model.util.SampleDataUtil; +import spleetwaise.address.storage.AddressBookStorage; +import spleetwaise.address.storage.JsonAddressBookStorage; +import spleetwaise.commons.core.Config; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.core.Version; +import spleetwaise.commons.exceptions.DataLoadingException; +import spleetwaise.commons.logic.Logic; +import spleetwaise.commons.logic.LogicManager; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.commons.model.UserPrefs; +import spleetwaise.commons.storage.JsonUserPrefsStorage; +import spleetwaise.commons.storage.Storage; +import spleetwaise.commons.storage.StorageManager; +import spleetwaise.commons.storage.UserPrefsStorage; +import spleetwaise.commons.ui.Ui; +import spleetwaise.commons.ui.UiManager; +import spleetwaise.commons.util.ConfigUtil; +import spleetwaise.commons.util.StringUtil; +import spleetwaise.transaction.model.ReadOnlyTransactionBook; +import spleetwaise.transaction.model.TransactionBook; +import spleetwaise.transaction.model.TransactionBookModel; +import spleetwaise.transaction.model.TransactionBookModelManager; +import spleetwaise.transaction.storage.JsonTransactionBookStorage; +import spleetwaise.transaction.storage.TransactionBookStorage; + +/** + * Runs the application. + */ +public class MainApp extends Application { + + public static final Version VERSION = new Version(1, 5, 1, true); + + private static final Logger logger = LogsCenter.getLogger(MainApp.class); + + protected Ui ui; + protected Logic logic; + protected Storage storage; + + protected AddressBookModel addressBookModel; + protected TransactionBookModel transactionBookModel; + + protected Config config; + + private volatile boolean isStopped = false; + + @Override + public void init() throws Exception { + logger.info("=============================[ Initializing SpleetWaise]==========================="); + super.init(); + + AppParameters appParameters = AppParameters.parse(getParameters()); + config = initConfig(appParameters.getConfigPath()); + initLogging(config); + + UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); + UserPrefs userPrefs = initPrefs(userPrefsStorage); + AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath()); + TransactionBookStorage transactionBookStorage = + new JsonTransactionBookStorage(userPrefs.getTransactionBookFilePath()); + storage = new StorageManager(addressBookStorage, userPrefsStorage, transactionBookStorage); + + initModelManagers(storage); + + // Initialise Common Model + CommonModelManager.initialise(addressBookModel, transactionBookModel, userPrefs); + + logic = new LogicManager(storage); + + ui = new UiManager(logic); + + // Add shutdown hook + Runtime.getRuntime().addShutdownHook(new Thread(this::cleanUp)); + } + + private void initModelManagers(Storage storage) { + logger.info("Using Ab file : " + storage.getAddressBookFilePath()); + logger.info("Using Tb file : " + storage.getTransactionBookFilePath()); + + try { + Optional addressBookOptional = storage.readAddressBook(); + if (!addressBookOptional.isPresent()) { + // If the addressbook is not present start from sample data + logger.info("Creating new data files " + storage.getAddressBookFilePath() + " and " + + storage.getTransactionBookFilePath() + + " populated with sample data."); + addressBookModel = new AddressBookModelManager(SampleDataUtil.getSampleAddressBook()); + // Since addressbook is nonexistent, whatever state transactionbook is in should become irrelevant. + // We shall therefore start from sample data with transactionbook as well. + transactionBookModel = new TransactionBookModelManager(SampleDataUtil.getSampleTransactionBook()); + return; + } + // Read the address book + addressBookModel = new AddressBookModelManager(addressBookOptional.get()); + Optional txnBookOptional = storage.readTransactionBook(addressBookModel); + if (!txnBookOptional.isPresent()) { + logger.info("Transaction book was not found and will be initialised as empty."); + } + // Read the transaction book + ReadOnlyTransactionBook transactionBook = txnBookOptional.orElse(new TransactionBook()); + transactionBookModel = new TransactionBookModelManager(transactionBook); + } catch (DataLoadingException e) { + // Files could be corrupted beyond recovery, and therefore we should start from a blank slate + logger.warning("Data files could not be loaded: " + e.getMessage() + + " Will be starting with an empty AddressBook and TransactionBook."); + addressBookModel = new AddressBookModelManager(new AddressBook()); + transactionBookModel = new TransactionBookModelManager(new TransactionBook()); + } + } + + private void initLogging(Config config) { + LogsCenter.init(config); + } + + /** + * Returns a {@code Config} using the file at {@code configFilePath}.
The default file path + * {@code Config#DEFAULT_CONFIG_FILE} will be used instead if {@code configFilePath} is null. + */ + protected Config initConfig(Path configFilePath) { + Config initializedConfig; + Path configFilePathUsed; + + configFilePathUsed = Config.DEFAULT_CONFIG_FILE; + + if (configFilePath != null) { + logger.info("Custom Config file specified " + configFilePath); + configFilePathUsed = configFilePath; + } + + logger.info("Using config file : " + configFilePathUsed); + + try { + Optional configOptional = ConfigUtil.readConfig(configFilePathUsed); + if (!configOptional.isPresent()) { + logger.info("Creating new config file " + configFilePathUsed); + } + initializedConfig = configOptional.orElse(new Config()); + } catch (DataLoadingException e) { + logger.warning("Config file at " + configFilePathUsed + " could not be loaded." + + " Using default config properties."); + initializedConfig = new Config(); + } + + //Update config file in case it was missing to begin with or there are new/unused fields + try { + ConfigUtil.saveConfig(initializedConfig, configFilePathUsed); + } catch (IOException e) { + logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + } + return initializedConfig; + } + + /** + * Returns a {@code UserPrefs} using the file at {@code storage}'s user prefs file path, or a new {@code UserPrefs} + * with default configuration if errors occur when reading from the file. + */ + protected UserPrefs initPrefs(UserPrefsStorage storage) { + Path prefsFilePath = storage.getUserPrefsFilePath(); + logger.info("Using preference file : " + prefsFilePath); + + UserPrefs initializedPrefs; + try { + Optional prefsOptional = storage.readUserPrefs(); + if (!prefsOptional.isPresent()) { + logger.info("Creating new preference file " + prefsFilePath); + } + initializedPrefs = prefsOptional.orElse(new UserPrefs()); + } catch (DataLoadingException e) { + logger.warning("Preference file at " + prefsFilePath + " could not be loaded." + + " Using default preferences."); + initializedPrefs = new UserPrefs(); + } + + //Update prefs file in case it was missing to begin with or there are new/unused fields + try { + storage.saveUserPrefs(initializedPrefs); + } catch (IOException e) { + logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + } + + return initializedPrefs; + } + + @Override + public void start(Stage primaryStage) { + logger.info("Starting SpleetWaise " + MainApp.VERSION); + ui.start(primaryStage); + } + + private void cleanUp() { + if (!isStopped) { + logger.info("Cleaning up resources..."); + try { + storage.saveUserPrefs(CommonModelManager.getInstance().getUserPrefs()); + storage.saveAddressBook(addressBookModel.getAddressBook()); + storage.saveTransactionBook(transactionBookModel.getTransactionBook()); + logger.info("Resources saved..."); + } catch (AccessDeniedException e) { + logger.severe(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage())); + } catch (IOException e) { + logger.severe(String.format(FILE_OPS_ERROR_FORMAT, e.getMessage())); + } + } + } + + @Override + public void stop() { + logger.info("============================ [ Stopping SpleetWaise ] ============================="); + cleanUp(); + isStopped = true; // Ensures `cleanUp()` only runs once + } +} diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/spleetwaise/commons/core/Config.java similarity index 94% rename from src/main/java/seedu/address/commons/core/Config.java rename to src/main/java/spleetwaise/commons/core/Config.java index 485f85a5e05..43d3ce1e732 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/spleetwaise/commons/core/Config.java @@ -1,11 +1,11 @@ -package seedu.address.commons.core; +package spleetwaise.commons.core; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Objects; import java.util.logging.Level; -import seedu.address.commons.util.ToStringBuilder; +import spleetwaise.commons.util.ToStringBuilder; /** * Config values used by the app @@ -62,5 +62,4 @@ public String toString() { .add("userPrefsFilePath", userPrefsFilePath) .toString(); } - } diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/spleetwaise/commons/core/GuiSettings.java similarity index 92% rename from src/main/java/seedu/address/commons/core/GuiSettings.java rename to src/main/java/spleetwaise/commons/core/GuiSettings.java index a97a86ee8d7..2873406ba90 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/spleetwaise/commons/core/GuiSettings.java @@ -1,14 +1,13 @@ -package seedu.address.commons.core; +package spleetwaise.commons.core; import java.awt.Point; import java.io.Serializable; import java.util.Objects; -import seedu.address.commons.util.ToStringBuilder; +import spleetwaise.commons.util.ToStringBuilder; /** - * A Serializable class that contains the GUI settings. - * Guarantees: immutable. + * A Serializable class that contains the GUI settings. Guarantees: immutable. */ public class GuiSettings implements Serializable { diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/spleetwaise/commons/core/LogsCenter.java similarity index 80% rename from src/main/java/seedu/address/commons/core/LogsCenter.java rename to src/main/java/spleetwaise/commons/core/LogsCenter.java index 8cf8e15a0f0..3afaba250e6 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/spleetwaise/commons/core/LogsCenter.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package spleetwaise.commons.core; import static java.util.Objects.requireNonNull; @@ -11,16 +11,15 @@ import java.util.logging.SimpleFormatter; /** - * Configures and manages loggers and handlers, including their logging level - * Named {@link Logger}s can be obtained from this class
- * These loggers have been configured to output messages to the console and a {@code .log} file by default, - * at the {@code INFO} level. A new {@code .log} file with a new numbering will be created after the log - * file reaches 5MB big, up to a maximum of 5 files.
+ * Configures and manages loggers and handlers, including their logging level Named {@link Logger}s can be obtained from + * this class
These loggers have been configured to output messages to the console and a {@code .log} file by + * default, at the {@code INFO} level. A new {@code .log} file with a new numbering will be created after the log file + * reaches 5MB big, up to a maximum of 5 files.
*/ public class LogsCenter { private static final int MAX_FILE_COUNT = 5; private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB - private static final String LOG_FILE = "addressbook.log"; + private static final String LOG_FILE = "spleetwaise.log"; private static final Logger logger; // logger for this class private static Logger baseLogger; // to be used as the parent of all other loggers created by this class. private static Level currentLogLevel = Level.INFO; @@ -43,9 +42,9 @@ public static void init(Config config) { } /** - * Creates a logger with the given name prefixed by the {@code baseLogger}'s name so that the created logger - * becomes a descendant of the {@code baseLogger}. Furthermore, the returned logger will have the same log handlers - * as the {@code baseLogger}. + * Creates a logger with the given name prefixed by the {@code baseLogger}'s name so that the created logger becomes + * a descendant of the {@code baseLogger}. Furthermore, the returned logger will have the same log handlers as the + * {@code baseLogger}. */ public static Logger getLogger(String name) { // Java organizes loggers into a hierarchy based on their names (using '.' as a separator, similar to how Java @@ -75,11 +74,11 @@ private static void removeHandlers(Logger logger) { } /** - * Creates a logger named 'ab3', containing a {@code ConsoleHandler} and a {@code FileHandler}. - * Sets it as the {@code baseLogger}, to be used as the parent logger of all other loggers. + * Creates a logger named 'spleetwaise', containing a {@code ConsoleHandler} and a {@code FileHandler}. Sets it as + * the {@code baseLogger}, to be used as the parent logger of all other loggers. */ private static void setBaseLogger() { - baseLogger = Logger.getLogger("ab3"); + baseLogger = Logger.getLogger("spleetwaise"); baseLogger.setUseParentHandlers(false); removeHandlers(baseLogger); @@ -101,6 +100,4 @@ private static void setBaseLogger() { logger.warning("Error adding file handler for logger."); } } - - } diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/spleetwaise/commons/core/Version.java similarity index 94% rename from src/main/java/seedu/address/commons/core/Version.java rename to src/main/java/spleetwaise/commons/core/Version.java index 491d24559b4..7a671d122da 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/spleetwaise/commons/core/Version.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package spleetwaise.commons.core; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -32,24 +32,9 @@ public Version(int major, int minor, int patch, boolean isEarlyAccess) { this.isEarlyAccess = isEarlyAccess; } - public int getMajor() { - return major; - } - - public int getMinor() { - return minor; - } - - public int getPatch() { - return patch; - } - - public boolean isEarlyAccess() { - return isEarlyAccess; - } - /** * Parses a version number string in the format V1.2.3. + * * @param versionString version number string * @return a Version object */ @@ -61,10 +46,28 @@ public static Version fromString(String versionString) throws IllegalArgumentExc throw new IllegalArgumentException(String.format(EXCEPTION_STRING_NOT_VERSION, versionString)); } - return new Version(Integer.parseInt(versionMatcher.group(1)), + return new Version( + Integer.parseInt(versionMatcher.group(1)), Integer.parseInt(versionMatcher.group(2)), Integer.parseInt(versionMatcher.group(3)), - versionMatcher.group(4) == null ? false : true); + versionMatcher.group(4) == null ? false : true + ); + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public int getPatch() { + return patch; + } + + public boolean isEarlyAccess() { + return isEarlyAccess; } @JsonValue diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/spleetwaise/commons/core/index/Index.java similarity index 79% rename from src/main/java/seedu/address/commons/core/index/Index.java rename to src/main/java/spleetwaise/commons/core/index/Index.java index dd170d8b68d..4143480ba95 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/spleetwaise/commons/core/index/Index.java @@ -1,21 +1,20 @@ -package seedu.address.commons.core.index; +package spleetwaise.commons.core.index; -import seedu.address.commons.util.ToStringBuilder; +import spleetwaise.commons.util.ToStringBuilder; /** * Represents a zero-based or one-based index. * * {@code Index} should be used right from the start (when parsing in a new user input), so that if the current - * component wants to communicate with another component, it can send an {@code Index} to avoid having to know what - * base the other component is using for its index. However, after receiving the {@code Index}, that component can - * convert it back to an int if the index will not be passed to a different component again. + * component wants to communicate with another component, it can send an {@code Index} to avoid having to know what base + * the other component is using for its index. However, after receiving the {@code Index}, that component can convert it + * back to an int if the index will not be passed to a different component again. */ public class Index { private int zeroBasedIndex; /** - * Index can only be created by calling {@link Index#fromZeroBased(int)} or - * {@link Index#fromOneBased(int)}. + * Index can only be created by calling {@link Index#fromZeroBased(int)} or {@link Index#fromOneBased(int)}. */ private Index(int zeroBasedIndex) { if (zeroBasedIndex < 0) { @@ -25,14 +24,6 @@ private Index(int zeroBasedIndex) { this.zeroBasedIndex = zeroBasedIndex; } - public int getZeroBased() { - return zeroBasedIndex; - } - - public int getOneBased() { - return zeroBasedIndex + 1; - } - /** * Creates a new {@code Index} using a zero-based index. */ @@ -47,6 +38,14 @@ public static Index fromOneBased(int oneBasedIndex) { return new Index(oneBasedIndex - 1); } + public int getZeroBased() { + return zeroBasedIndex; + } + + public int getOneBased() { + return zeroBasedIndex + 1; + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java b/src/main/java/spleetwaise/commons/exceptions/DataLoadingException.java similarity index 82% rename from src/main/java/seedu/address/commons/exceptions/DataLoadingException.java rename to src/main/java/spleetwaise/commons/exceptions/DataLoadingException.java index 9904ba47afe..c4180bae4e2 100644 --- a/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java +++ b/src/main/java/spleetwaise/commons/exceptions/DataLoadingException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package spleetwaise.commons.exceptions; /** * Represents an error during loading of data from a file. diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java b/src/main/java/spleetwaise/commons/exceptions/IllegalValueException.java similarity index 86% rename from src/main/java/seedu/address/commons/exceptions/IllegalValueException.java rename to src/main/java/spleetwaise/commons/exceptions/IllegalValueException.java index 19124db485c..a28da340e15 100644 --- a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java +++ b/src/main/java/spleetwaise/commons/exceptions/IllegalValueException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package spleetwaise.commons.exceptions; /** * Signals that some given data does not fulfill some constraints. @@ -13,7 +13,7 @@ public IllegalValueException(String message) { /** * @param message should contain relevant information on the failed constraint(s) - * @param cause of the main exception + * @param cause of the main exception */ public IllegalValueException(String message, Throwable cause) { super(message, cause); diff --git a/src/main/java/spleetwaise/commons/exceptions/SpleetWaiseCommandException.java b/src/main/java/spleetwaise/commons/exceptions/SpleetWaiseCommandException.java new file mode 100644 index 00000000000..036ca59180e --- /dev/null +++ b/src/main/java/spleetwaise/commons/exceptions/SpleetWaiseCommandException.java @@ -0,0 +1,14 @@ +package spleetwaise.commons.exceptions; + +/** + * Represents an exception that occurs during command execution in SpleetWaise. + */ +public class SpleetWaiseCommandException extends Exception { + public SpleetWaiseCommandException(String message) { + super(message); + } + + public SpleetWaiseCommandException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/spleetwaise/commons/logic/Logic.java similarity index 50% rename from src/main/java/seedu/address/logic/Logic.java rename to src/main/java/spleetwaise/commons/logic/Logic.java index 92cd8fa605a..e999265459b 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/spleetwaise/commons/logic/Logic.java @@ -1,14 +1,17 @@ -package seedu.address.logic; +package spleetwaise.commons.logic; import java.nio.file.Path; import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; +import spleetwaise.address.model.AddressBookModel; +import spleetwaise.address.model.ReadOnlyAddressBook; +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.core.GuiSettings; +import spleetwaise.commons.exceptions.SpleetWaiseCommandException; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.transaction.model.transaction.Transaction; /** * API of the Logic component @@ -16,17 +19,18 @@ public interface Logic { /** * Executes the command and returns the result. + * * @param commandText The command as entered by the user. * @return the result of the command execution. * @throws CommandException If an error occurs during command execution. - * @throws ParseException If an error occurs during parsing. + * @throws ParseException If an error occurs during parsing. */ - CommandResult execute(String commandText) throws CommandException, ParseException; + CommandResult execute(String commandText) throws SpleetWaiseCommandException, ParseException; /** * Returns the AddressBook. * - * @see seedu.address.model.Model#getAddressBook() + * @see AddressBookModel#getAddressBook() */ ReadOnlyAddressBook getAddressBook(); @@ -47,4 +51,7 @@ public interface Logic { * Set the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); + + /** Returns an unmodifiable view of the filtered list of transactions */ + ObservableList getFilteredTransactionList(); } diff --git a/src/main/java/spleetwaise/commons/logic/LogicManager.java b/src/main/java/spleetwaise/commons/logic/LogicManager.java new file mode 100644 index 00000000000..0e86592dd71 --- /dev/null +++ b/src/main/java/spleetwaise/commons/logic/LogicManager.java @@ -0,0 +1,111 @@ +package spleetwaise.commons.logic; + +import static spleetwaise.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; + +import java.io.IOException; +import java.nio.file.AccessDeniedException; +import java.nio.file.Path; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import spleetwaise.address.logic.parser.AddressBookParser; +import spleetwaise.address.model.ReadOnlyAddressBook; +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.core.GuiSettings; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.exceptions.SpleetWaiseCommandException; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.commons.model.CommonModel; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.commons.storage.Storage; +import spleetwaise.transaction.logic.parser.TransactionBookParser; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * The main LogicManager of the app. + */ +public class LogicManager implements Logic { + + public static final String FILE_OPS_ERROR_FORMAT = "Could not save data due to the following error: %s"; + + public static final String FILE_OPS_PERMISSION_ERROR_FORMAT = + "Could not save data to file %s due to insufficient permissions to write to the file or the folder."; + + private final Logger logger = LogsCenter.getLogger(LogicManager.class); + private final Storage storage; + private final AddressBookParser addressBookParser; + private final TransactionBookParser transactionParser; + + /** + * Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}. + */ + public LogicManager(Storage storage) { + this.storage = storage; + this.addressBookParser = new AddressBookParser(); + this.transactionParser = new TransactionBookParser(); + } + + @Override + public CommandResult execute(String commandText) throws ParseException, SpleetWaiseCommandException { + logger.info("----------------[USER COMMAND][" + commandText + "]"); + + // Parse command + Command command; + command = addressBookParser.parseCommand(commandText); + if (command == null) { + command = transactionParser.parseCommand(commandText); + } + if (command == null) { + throw new ParseException(String.format(MESSAGE_UNKNOWN_COMMAND, commandText)); + } + + // Execute command + CommandResult commandResult = command.execute(); + + // Update storage + try { + CommonModel model = CommonModelManager.getInstance(); + storage.saveTransactionBook(model.getTransactionBook()); + storage.saveAddressBook(model.getAddressBook()); + } catch (AccessDeniedException e) { + throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e); + } catch (IOException ioe) { + throw new CommandException(String.format(FILE_OPS_ERROR_FORMAT, ioe.getMessage()), ioe); + } + + return commandResult; + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + return CommonModelManager.getInstance().getAddressBook(); + } + + @Override + public ObservableList getFilteredPersonList() { + return CommonModelManager.getInstance().getFilteredPersonList(); + } + + @Override + public Path getAddressBookFilePath() { + return CommonModelManager.getInstance().getAddressBookFilePath(); + } + + @Override + public GuiSettings getGuiSettings() { + return CommonModelManager.getInstance().getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + CommonModelManager.getInstance().setGuiSettings(guiSettings); + } + + @Override + public ObservableList getFilteredTransactionList() { + return CommonModelManager.getInstance().getFilteredTransactionList(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/spleetwaise/commons/logic/commands/Command.java similarity index 55% rename from src/main/java/seedu/address/logic/commands/Command.java rename to src/main/java/spleetwaise/commons/logic/commands/Command.java index 64f18992160..fca2d420b5b 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/spleetwaise/commons/logic/commands/Command.java @@ -1,7 +1,6 @@ -package seedu.address.logic.commands; +package spleetwaise.commons.logic.commands; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; +import spleetwaise.commons.logic.commands.exceptions.CommandException; /** * Represents a command with hidden internal logic and the ability to be executed. @@ -11,10 +10,9 @@ public abstract class Command { /** * Executes the command and returns the result message. * - * @param model {@code Model} which the command should operate on. * @return feedback message of the operation result for display * @throws CommandException If an error occurs during command execution. */ - public abstract CommandResult execute(Model model) throws CommandException; + public abstract CommandResult execute() throws CommandException; } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/spleetwaise/commons/logic/commands/CommandResult.java similarity index 92% rename from src/main/java/seedu/address/logic/commands/CommandResult.java rename to src/main/java/spleetwaise/commons/logic/commands/CommandResult.java index 249b6072d0d..a4a04182b70 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/spleetwaise/commons/logic/commands/CommandResult.java @@ -1,10 +1,10 @@ -package seedu.address.logic.commands; +package spleetwaise.commons.logic.commands; import static java.util.Objects.requireNonNull; import java.util.Objects; -import seedu.address.commons.util.ToStringBuilder; +import spleetwaise.commons.util.ToStringBuilder; /** * Represents the result of a command execution. @@ -29,8 +29,8 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { } /** - * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, - * and other fields set to their default value. + * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, and other fields set to their + * default value. */ public CommandResult(String feedbackToUser) { this(feedbackToUser, false, false); diff --git a/src/main/java/spleetwaise/commons/logic/commands/CommandUtil.java b/src/main/java/spleetwaise/commons/logic/commands/CommandUtil.java new file mode 100644 index 00000000000..7b0b401c6f4 --- /dev/null +++ b/src/main/java/spleetwaise/commons/logic/commands/CommandUtil.java @@ -0,0 +1,38 @@ +package spleetwaise.commons.logic.commands; + +import static spleetwaise.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; + +import spleetwaise.address.logic.Messages; +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.model.CommonModelManager; + +/** + * Utility class for common command-related operations. Contains helper methods to reduce code duplication in command + * classes. + */ +public class CommandUtil { + + /** + * Retrieves a {@code Person} from the filtered person list at the specified {@code Index}. + * + * @param model The {@code CommonModelManager} instance used to retrieve the filtered person list. + * @param index The {@code Index} of the person to retrieve from the filtered address book list. + * @return The {@code Person} at the specified index in the filtered person list. + * @throws CommandException If the specified index is out of bounds for the filtered person list. + */ + public static Person getPersonByFilteredPersonListIndex(CommonModelManager model, Index index) + throws CommandException { + requireAllNonNull(model, index); + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + return lastShownList.get(index.getZeroBased()); + } +} diff --git a/src/main/java/spleetwaise/commons/logic/commands/exceptions/CommandException.java b/src/main/java/spleetwaise/commons/logic/commands/exceptions/CommandException.java new file mode 100644 index 00000000000..d70b724ad82 --- /dev/null +++ b/src/main/java/spleetwaise/commons/logic/commands/exceptions/CommandException.java @@ -0,0 +1,16 @@ +package spleetwaise.commons.logic.commands.exceptions; + +import spleetwaise.commons.exceptions.SpleetWaiseCommandException; + +/** + * Represents an error that occurs during command execution in the address component. + */ +public class CommandException extends SpleetWaiseCommandException { + public CommandException(String message) { + super(message); + } + + public CommandException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/spleetwaise/commons/logic/parser/BaseParserUtil.java b/src/main/java/spleetwaise/commons/logic/parser/BaseParserUtil.java new file mode 100644 index 00000000000..d928fda644e --- /dev/null +++ b/src/main/java/spleetwaise/commons/logic/parser/BaseParserUtil.java @@ -0,0 +1,45 @@ +package spleetwaise.commons.logic.parser; + +import java.util.stream.Stream; + +import spleetwaise.address.logic.parser.ArgumentMultimap; +import spleetwaise.address.logic.parser.Prefix; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.commons.util.StringUtil; + +/** + * Contains utility methods used for parsing strings in the various *Parser classes. + */ +public class BaseParserUtil { + public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + + /** + * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + */ + public static Index parseIndex(String oneBasedIndex) throws ParseException { + String trimmedIndex = oneBasedIndex.trim(); + if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + return Index.fromOneBased(Integer.parseInt(trimmedIndex)); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + public static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + /** + * Returns true if any of the prefixes are present in the given {@code ArgumentMultimap}. + */ + public static boolean areAnyPrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).anyMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/spleetwaise/commons/logic/parser/Parser.java similarity index 64% rename from src/main/java/seedu/address/logic/parser/Parser.java rename to src/main/java/spleetwaise/commons/logic/parser/Parser.java index d6551ad8e3f..7ff81361d6e 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/spleetwaise/commons/logic/parser/Parser.java @@ -1,7 +1,7 @@ -package seedu.address.logic.parser; +package spleetwaise.commons.logic.parser; -import seedu.address.logic.commands.Command; -import seedu.address.logic.parser.exceptions.ParseException; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.parser.exceptions.ParseException; /** * Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}. @@ -10,7 +10,8 @@ public interface Parser { /** * Parses {@code userInput} into a command and returns it. - * @throws ParseException if {@code userInput} does not conform the expected format + * + * @throws ParseException if {@code userInput} does not conform the expected format. */ T parse(String userInput) throws ParseException; } diff --git a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java b/src/main/java/spleetwaise/commons/logic/parser/exceptions/ParseException.java similarity index 72% rename from src/main/java/seedu/address/logic/parser/exceptions/ParseException.java rename to src/main/java/spleetwaise/commons/logic/parser/exceptions/ParseException.java index 158a1a54c1c..6203732e651 100644 --- a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java +++ b/src/main/java/spleetwaise/commons/logic/parser/exceptions/ParseException.java @@ -1,6 +1,6 @@ -package seedu.address.logic.parser.exceptions; +package spleetwaise.commons.logic.parser.exceptions; -import seedu.address.commons.exceptions.IllegalValueException; +import spleetwaise.commons.exceptions.IllegalValueException; /** * Represents a parse error encountered by a parser. diff --git a/src/main/java/spleetwaise/commons/model/CommonModel.java b/src/main/java/spleetwaise/commons/model/CommonModel.java new file mode 100644 index 00000000000..6408a5dc0b9 --- /dev/null +++ b/src/main/java/spleetwaise/commons/model/CommonModel.java @@ -0,0 +1,45 @@ +package spleetwaise.commons.model; + +import java.nio.file.Path; + +import spleetwaise.address.model.AddressBookModel; +import spleetwaise.commons.core.GuiSettings; +import spleetwaise.transaction.model.TransactionBookModel; + +/** + *

Interface representing the model layer that encapsulates the business logic.

+ * + * @see AddressBookModel + * @see TransactionBookModel + */ +public interface CommonModel extends AddressBookModel, TransactionBookModel { + /** + * Returns the user prefs. + */ + ReadOnlyUserPrefs getUserPrefs(); + + /** + * Replaces user prefs data with the data in {@code userPrefs}. + */ + void setUserPrefs(ReadOnlyUserPrefs userPrefs); + + /** + * Returns the user prefs' GUI settings. + */ + GuiSettings getGuiSettings(); + + /** + * Sets the user prefs' GUI settings. + */ + void setGuiSettings(GuiSettings guiSettings); + + /** + * Returns the user prefs' address book file path. + */ + Path getAddressBookFilePath(); + + /** + * Sets the user prefs' address book file path. + */ + void setAddressBookFilePath(Path addressBookFilePath); +} diff --git a/src/main/java/spleetwaise/commons/model/CommonModelManager.java b/src/main/java/spleetwaise/commons/model/CommonModelManager.java new file mode 100644 index 00000000000..b2b05b33945 --- /dev/null +++ b/src/main/java/spleetwaise/commons/model/CommonModelManager.java @@ -0,0 +1,230 @@ +package spleetwaise.commons.model; + +import static java.util.Objects.requireNonNull; +import static spleetwaise.commons.util.CollectionUtil.requireAllNonNull; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Predicate; + +import javafx.beans.property.ObjectProperty; +import javafx.collections.ObservableList; +import spleetwaise.address.model.AddressBookModel; +import spleetwaise.address.model.ReadOnlyAddressBook; +import spleetwaise.address.model.person.Person; +import spleetwaise.address.model.person.Phone; +import spleetwaise.commons.core.GuiSettings; +import spleetwaise.transaction.model.ReadOnlyTransactionBook; +import spleetwaise.transaction.model.TransactionBookModel; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * The CommonModelManager class represents the common model that will be used by multiple parts of the application. + */ +public class CommonModelManager implements CommonModel { + // Singleton instance + private static CommonModelManager model = null; + + private final UserPrefs userPrefs; + private AddressBookModel addressBookModel; + private TransactionBookModel transactionBookModel; + + private CommonModelManager(AddressBookModel abModel, TransactionBookModel tbModel, ReadOnlyUserPrefs userPrefs) { + addressBookModel = abModel; + transactionBookModel = tbModel; + this.userPrefs = new UserPrefs(userPrefs); + } + + public static synchronized CommonModelManager getInstance() { + requireNonNull(model); + return model; + } + + /** + * Initialises the singleton instance of this class with the given address book and transaction book models. + * + * @param abModel The address book model to use + * @param tbModel The transaction book model to use + * @param userPrefs The user prefs to use + */ + public static synchronized void initialise( + AddressBookModel abModel, TransactionBookModel tbModel, + ReadOnlyUserPrefs userPrefs + ) { + model = new CommonModelManager(abModel, tbModel, userPrefs); + } + + + public static synchronized void initialise(AddressBookModel abModel, TransactionBookModel tbModel) { + model = new CommonModelManager(abModel, tbModel, new UserPrefs()); + } + + /** + * De-initialises the singleton instance of this class. + */ + public static synchronized void deinitialise() { + model = null; + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + return userPrefs; + } + + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + requireNonNull(userPrefs); + this.userPrefs.resetData(userPrefs); + } + + @Override + public GuiSettings getGuiSettings() { + return userPrefs.getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + requireNonNull(guiSettings); + userPrefs.setGuiSettings(guiSettings); + } + + @Override + public Path getAddressBookFilePath() { + return userPrefs.getAddressBookFilePath(); + } + + @Override + public void setAddressBookFilePath(Path addressBookFilePath) { + requireNonNull(addressBookFilePath); + userPrefs.setAddressBookFilePath(addressBookFilePath); + } + + @Override + public ReadOnlyAddressBook getAddressBook() { + requireNonNull(addressBookModel, "AddressBook model cannot be null"); + return addressBookModel.getAddressBook(); + } + + @Override + public void setAddressBook(ReadOnlyAddressBook addressBook) { + requireNonNull(addressBookModel, "AddressBook model cannot be null"); + addressBookModel.setAddressBook(addressBook); + } + + @Override + public boolean hasPerson(Person person) { + requireNonNull(addressBookModel, "AddressBook model cannot be null"); + return addressBookModel.hasPerson(person); + } + + @Override + public void deletePerson(Person target) { + requireNonNull(addressBookModel, "AddressBook model cannot be null"); + addressBookModel.deletePerson(target); + } + + @Override + public void addPerson(Person person) { + requireNonNull(addressBookModel, "AddressBook model cannot be null"); + addressBookModel.addPerson(person); + } + + @Override + public void setPerson(Person target, Person editedPerson) { + requireNonNull(addressBookModel, "AddressBook model cannot be null"); + addressBookModel.setPerson(target, editedPerson); + } + + @Override + public ObservableList getFilteredPersonList() { + requireNonNull(addressBookModel, "AddressBook model cannot be null"); + return addressBookModel.getFilteredPersonList(); + } + + @Override + public void updateFilteredPersonList(Predicate predicate) { + requireNonNull(addressBookModel, "AddressBook model cannot be null"); + addressBookModel.updateFilteredPersonList(predicate); + } + + @Override + public Optional getPersonById(String id) { + requireNonNull(addressBookModel, "AddressBook model cannot be null"); + return addressBookModel.getPersonById(id); + } + + @Override + public Optional getPersonByPhone(Phone phone) { + requireNonNull(addressBookModel, "AddressBook model cannot be null"); + requireNonNull(phone); + return addressBookModel.getPersonByPhone(phone); + } + + // TransactionBook + @Override + public ReadOnlyTransactionBook getTransactionBook() { + requireNonNull(transactionBookModel, "TransactionBook model cannot be null"); + return transactionBookModel.getTransactionBook(); + } + + @Override + public void setTransactionBook(ReadOnlyTransactionBook replacementBook) { + requireNonNull(transactionBookModel, "TransactionBook model cannot be null"); + transactionBookModel.setTransactionBook(replacementBook); + } + + @Override + public void addTransaction(Transaction transaction) { + requireNonNull(transactionBookModel, "TransactionBook model cannot be null"); + transactionBookModel.addTransaction(transaction); + } + + @Override + public boolean hasTransaction(Transaction transaction) { + requireNonNull(transactionBookModel, "TransactionBook model cannot be null"); + return transactionBookModel.hasTransaction(transaction); + } + + @Override + public ObservableList getFilteredTransactionList() { + requireNonNull(transactionBookModel, "TransactionBook model cannot be null"); + return transactionBookModel.getFilteredTransactionList(); + } + + @Override + public ObjectProperty> getCurrentPredicate() { + requireNonNull(transactionBookModel, "TransactionBook model cannot be null"); + return transactionBookModel.getCurrentPredicate(); + } + + @Override + public void updateFilteredTransactionList() { + requireNonNull(transactionBookModel, "TransactionBook model cannot be null"); + transactionBookModel.updateFilteredTransactionList(); + } + + @Override + public void updateFilteredTransactionList(Predicate predicate) { + requireNonNull(transactionBookModel, "TransactionBook model cannot be null"); + transactionBookModel.updateFilteredTransactionList(predicate); + } + + @Override + public void deleteTransactionsOfPersonId(String personId) { + requireNonNull(personId); + transactionBookModel.deleteTransactionsOfPersonId(personId); + } + + @Override + public void deleteTransaction(Transaction target) { + requireNonNull(transactionBookModel, "TransactionBook model cannot be null"); + transactionBookModel.deleteTransaction(target); + } + + @Override + public void setTransaction(Transaction target, Transaction editedTransaction) { + requireNonNull(transactionBookModel, "TransactionBook model cannot be null"); + requireAllNonNull(target, editedTransaction); + transactionBookModel.setTransaction(target, editedTransaction); + } +} diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/spleetwaise/commons/model/ReadOnlyUserPrefs.java similarity index 60% rename from src/main/java/seedu/address/model/ReadOnlyUserPrefs.java rename to src/main/java/spleetwaise/commons/model/ReadOnlyUserPrefs.java index befd58a4c73..ddd79c2f5a1 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/spleetwaise/commons/model/ReadOnlyUserPrefs.java @@ -1,8 +1,8 @@ -package seedu.address.model; +package spleetwaise.commons.model; import java.nio.file.Path; -import seedu.address.commons.core.GuiSettings; +import spleetwaise.commons.core.GuiSettings; /** * Unmodifiable view of user prefs. @@ -13,4 +13,6 @@ public interface ReadOnlyUserPrefs { Path getAddressBookFilePath(); + Path getTransactionBookFilePath(); + } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/spleetwaise/commons/model/UserPrefs.java similarity index 66% rename from src/main/java/seedu/address/model/UserPrefs.java rename to src/main/java/spleetwaise/commons/model/UserPrefs.java index 6be655fb4c7..37a8fce28e0 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/spleetwaise/commons/model/UserPrefs.java @@ -1,4 +1,4 @@ -package seedu.address.model; +package spleetwaise.commons.model; import static java.util.Objects.requireNonNull; @@ -6,7 +6,7 @@ import java.nio.file.Paths; import java.util.Objects; -import seedu.address.commons.core.GuiSettings; +import spleetwaise.commons.core.GuiSettings; /** * Represents User's preferences. @@ -14,12 +14,14 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path addressBookFilePath = Paths.get("data", "addressbook.json"); + private Path transactionBookFilePath = Paths.get("data", "transactionbook.json"); /** * Creates a {@code UserPrefs} with default values. */ - public UserPrefs() {} + public UserPrefs() { + } /** * Creates a {@code UserPrefs} with the prefs in {@code userPrefs}. @@ -36,6 +38,7 @@ public void resetData(ReadOnlyUserPrefs newUserPrefs) { requireNonNull(newUserPrefs); setGuiSettings(newUserPrefs.getGuiSettings()); setAddressBookFilePath(newUserPrefs.getAddressBookFilePath()); + setTransactionBookFilePath(newUserPrefs.getTransactionBookFilePath()); } public GuiSettings getGuiSettings() { @@ -56,6 +59,15 @@ public void setAddressBookFilePath(Path addressBookFilePath) { this.addressBookFilePath = addressBookFilePath; } + public Path getTransactionBookFilePath() { + return transactionBookFilePath; + } + + public void setTransactionBookFilePath(Path transactionBookFilePath) { + requireNonNull(transactionBookFilePath); + this.transactionBookFilePath = transactionBookFilePath; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -69,19 +81,21 @@ public boolean equals(Object other) { UserPrefs otherUserPrefs = (UserPrefs) other; return guiSettings.equals(otherUserPrefs.guiSettings) - && addressBookFilePath.equals(otherUserPrefs.addressBookFilePath); + && addressBookFilePath.equals(otherUserPrefs.addressBookFilePath) + && transactionBookFilePath.equals(otherUserPrefs.transactionBookFilePath); } @Override public int hashCode() { - return Objects.hash(guiSettings, addressBookFilePath); + return Objects.hash(guiSettings, addressBookFilePath, transactionBookFilePath); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Gui Settings : " + guiSettings); - sb.append("\nLocal data file location : " + addressBookFilePath); + sb.append("\nLocal address book data file location : " + addressBookFilePath); + sb.append("\nLocal transaction book data file location : " + transactionBookFilePath); return sb.toString(); } diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/spleetwaise/commons/storage/JsonUserPrefsStorage.java similarity index 81% rename from src/main/java/seedu/address/storage/JsonUserPrefsStorage.java rename to src/main/java/spleetwaise/commons/storage/JsonUserPrefsStorage.java index 48a9754807d..60bfcdf300b 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/spleetwaise/commons/storage/JsonUserPrefsStorage.java @@ -1,13 +1,13 @@ -package seedu.address.storage; +package spleetwaise.commons.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import spleetwaise.commons.exceptions.DataLoadingException; +import spleetwaise.commons.model.ReadOnlyUserPrefs; +import spleetwaise.commons.model.UserPrefs; +import spleetwaise.commons.util.JsonUtil; /** * A class to access UserPrefs stored in the hard disk as a json file @@ -32,6 +32,7 @@ public Optional readUserPrefs() throws DataLoadingException { /** * Similar to {@link #readUserPrefs()} + * * @param prefsFilePath location of the data. Cannot be null. * @throws DataLoadingException if the file format is not as expected. */ diff --git a/src/main/java/spleetwaise/commons/storage/Storage.java b/src/main/java/spleetwaise/commons/storage/Storage.java new file mode 100644 index 00000000000..5bba23e79c1 --- /dev/null +++ b/src/main/java/spleetwaise/commons/storage/Storage.java @@ -0,0 +1,46 @@ +package spleetwaise.commons.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import spleetwaise.address.model.AddressBookModel; +import spleetwaise.address.model.ReadOnlyAddressBook; +import spleetwaise.address.storage.AddressBookStorage; +import spleetwaise.commons.exceptions.DataLoadingException; +import spleetwaise.commons.model.ReadOnlyUserPrefs; +import spleetwaise.commons.model.UserPrefs; +import spleetwaise.transaction.model.ReadOnlyTransactionBook; +import spleetwaise.transaction.storage.TransactionBookStorage; + +/** + * API of the Storage component + */ +public interface Storage extends AddressBookStorage, UserPrefsStorage, TransactionBookStorage { + + @Override + Optional readUserPrefs() throws DataLoadingException; + + @Override + void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; + + @Override + Path getAddressBookFilePath(); + + @Override + Optional readAddressBook() throws DataLoadingException; + + @Override + void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + + @Override + Path getTransactionBookFilePath(); + + @Override + Optional readTransactionBook(AddressBookModel addressBookModel) + throws DataLoadingException; + + @Override + void saveTransactionBook(ReadOnlyTransactionBook transactionBook) throws IOException; + +} diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/spleetwaise/commons/storage/StorageManager.java similarity index 51% rename from src/main/java/seedu/address/storage/StorageManager.java rename to src/main/java/spleetwaise/commons/storage/StorageManager.java index 8b84a9024d5..b81a0affb0a 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/spleetwaise/commons/storage/StorageManager.java @@ -1,15 +1,19 @@ -package seedu.address.storage; +package spleetwaise.commons.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; import java.util.logging.Logger; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import spleetwaise.address.model.AddressBookModel; +import spleetwaise.address.model.ReadOnlyAddressBook; +import spleetwaise.address.storage.AddressBookStorage; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.exceptions.DataLoadingException; +import spleetwaise.commons.model.ReadOnlyUserPrefs; +import spleetwaise.commons.model.UserPrefs; +import spleetwaise.transaction.model.ReadOnlyTransactionBook; +import spleetwaise.transaction.storage.TransactionBookStorage; /** * Manages storage of AddressBook data in local storage. @@ -19,13 +23,18 @@ public class StorageManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); private AddressBookStorage addressBookStorage; private UserPrefsStorage userPrefsStorage; + private TransactionBookStorage transactionBookStorage; /** * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}. */ - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { + public StorageManager( + AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage, + TransactionBookStorage transactionBookStorage + ) { this.addressBookStorage = addressBookStorage; this.userPrefsStorage = userPrefsStorage; + this.transactionBookStorage = transactionBookStorage; } // ================ UserPrefs methods ============================== @@ -45,7 +54,6 @@ public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { userPrefsStorage.saveUserPrefs(userPrefs); } - // ================ AddressBook methods ============================== @Override @@ -75,4 +83,34 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro addressBookStorage.saveAddressBook(addressBook, filePath); } + // ================ TransactionBook methods ============================== + + @Override + public Path getTransactionBookFilePath() { + return transactionBookStorage.getTransactionBookFilePath(); + } + + @Override + public Optional readTransactionBook(AddressBookModel abModel) throws DataLoadingException { + return readTransactionBook(transactionBookStorage.getTransactionBookFilePath(), abModel); + } + + @Override + public Optional readTransactionBook(Path filePath, AddressBookModel abModel) + throws DataLoadingException { + logger.fine("Attempting to read data from file: " + filePath); + return transactionBookStorage.readTransactionBook(filePath, abModel); + } + + @Override + public void saveTransactionBook(ReadOnlyTransactionBook transactionBook) throws IOException { + saveTransactionBook(transactionBook, transactionBookStorage.getTransactionBookFilePath()); + } + + @Override + public void saveTransactionBook(ReadOnlyTransactionBook transactionBook, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + transactionBookStorage.saveTransactionBook(transactionBook, filePath); + } + } diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/spleetwaise/commons/storage/UserPrefsStorage.java similarity index 58% rename from src/main/java/seedu/address/storage/UserPrefsStorage.java rename to src/main/java/spleetwaise/commons/storage/UserPrefsStorage.java index e94ca422ea8..2b4d7fc9f1c 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/spleetwaise/commons/storage/UserPrefsStorage.java @@ -1,15 +1,15 @@ -package seedu.address.storage; +package spleetwaise.commons.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataLoadingException; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import spleetwaise.commons.exceptions.DataLoadingException; +import spleetwaise.commons.model.ReadOnlyUserPrefs; +import spleetwaise.commons.model.UserPrefs; /** - * Represents a storage for {@link seedu.address.model.UserPrefs}. + * Represents a storage for {@link UserPrefs}. */ public interface UserPrefsStorage { @@ -19,15 +19,15 @@ public interface UserPrefsStorage { Path getUserPrefsFilePath(); /** - * Returns UserPrefs data from storage. - * Returns {@code Optional.empty()} if storage file is not found. + * Returns UserPrefs data from storage. Returns {@code Optional.empty()} if storage file is not found. * * @throws DataLoadingException if the loading of data from preference file failed. */ Optional readUserPrefs() throws DataLoadingException; /** - * Saves the given {@link seedu.address.model.ReadOnlyUserPrefs} to the storage. + * Saves the given {@link ReadOnlyUserPrefs} to the storage. + * * @param userPrefs cannot be null. * @throws IOException if there was any problem writing to the file. */ diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/spleetwaise/commons/ui/CommandBox.java similarity index 71% rename from src/main/java/seedu/address/ui/CommandBox.java rename to src/main/java/spleetwaise/commons/ui/CommandBox.java index 9e75478664b..08f6fb7db49 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/spleetwaise/commons/ui/CommandBox.java @@ -1,12 +1,13 @@ -package seedu.address.ui; +package spleetwaise.commons.ui; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; import javafx.scene.layout.Region; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; +import spleetwaise.commons.exceptions.SpleetWaiseCommandException; +import spleetwaise.commons.logic.Logic; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.parser.exceptions.ParseException; /** * The UI component that is responsible for receiving user command inputs. @@ -35,7 +36,7 @@ public CommandBox(CommandExecutor commandExecutor) { * Handles the Enter button pressed event. */ @FXML - private void handleCommandEntered() { + private void handleCommandEntered() throws SpleetWaiseCommandException, ParseException { String commandText = commandTextField.getText(); if (commandText.equals("")) { return; @@ -44,11 +45,20 @@ private void handleCommandEntered() { try { commandExecutor.execute(commandText); commandTextField.setText(""); - } catch (CommandException | ParseException e) { + } catch (SpleetWaiseCommandException | ParseException e) { setStyleToIndicateCommandFailure(); } } + /** + * Sets the command box with filter command text. + * + * @param commandText the filter command text. + */ + public void handleFilterCommandEntered(String commandText) { + commandTextField.setText(commandText); + } + /** * Sets the command box style to use the default style. */ @@ -77,9 +87,9 @@ public interface CommandExecutor { /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see Logic#execute(String) */ - CommandResult execute(String commandText) throws CommandException, ParseException; + CommandResult execute(String commandText) throws SpleetWaiseCommandException, ParseException; } } diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/spleetwaise/commons/ui/HelpWindow.java similarity index 61% rename from src/main/java/seedu/address/ui/HelpWindow.java rename to src/main/java/spleetwaise/commons/ui/HelpWindow.java index 3f16b2fcf26..09ee55016c1 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/spleetwaise/commons/ui/HelpWindow.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package spleetwaise.commons.ui; import java.util.logging.Logger; @@ -8,14 +8,14 @@ import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.stage.Stage; -import seedu.address.commons.core.LogsCenter; +import spleetwaise.commons.core.LogsCenter; /** * Controller for a help page */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2425s1-cs2103-f13-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); @@ -46,21 +46,22 @@ public HelpWindow() { /** * Shows the help window. - * @throws IllegalStateException - *
    - *
  • - * if this method is called on a thread other than the JavaFX Application Thread. - *
  • - *
  • - * if this method is called during animation or layout processing. - *
  • - *
  • - * if this method is called on the primary stage. - *
  • - *
  • - * if {@code dialogStage} is already showing. - *
  • - *
+ * + * @throws IllegalStateException
    + *
  • + * if this method is called on a thread other than the JavaFX + * Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
*/ public void show() { logger.fine("Showing help page about the application."); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/spleetwaise/commons/ui/MainWindow.java similarity index 82% rename from src/main/java/seedu/address/ui/MainWindow.java rename to src/main/java/spleetwaise/commons/ui/MainWindow.java index 79e74ef37c0..5072cee04e4 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/spleetwaise/commons/ui/MainWindow.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package spleetwaise.commons.ui; import java.util.logging.Logger; @@ -10,30 +10,31 @@ import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; import javafx.stage.Stage; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.Logic; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; +import spleetwaise.address.ui.PersonListPanel; +import spleetwaise.commons.core.GuiSettings; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.exceptions.SpleetWaiseCommandException; +import spleetwaise.commons.logic.Logic; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.transaction.ui.RightPanel; /** - * The Main Window. Provides the basic application layout containing - * a menu bar and space where other JavaFX elements can be placed. + * The Main Window. Provides the basic application layout containing a menu bar and space where other JavaFX elements + * can be placed. */ public class MainWindow extends UiPart { private static final String FXML = "MainWindow.fxml"; private final Logger logger = LogsCenter.getLogger(getClass()); - - private Stage primaryStage; - private Logic logic; - + private final Stage primaryStage; + private final Logic logic; + private final HelpWindow helpWindow; // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private RightPanel rightPanel; private ResultDisplay resultDisplay; - private HelpWindow helpWindow; @FXML private StackPane commandBoxPlaceholder; @@ -50,6 +51,9 @@ public class MainWindow extends UiPart { @FXML private StackPane statusbarPlaceholder; + @FXML + private StackPane rightPanelPlaceholder; + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ @@ -78,6 +82,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) { @@ -113,14 +118,18 @@ void fillInnerParts() { personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + CommandBox commandBox = new CommandBox(this::executeCommand); + commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + + rightPanel = new RightPanel(commandBox); + rightPanelPlaceholder.getChildren().add(rightPanel.getRoot()); + resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - CommandBox commandBox = new CommandBox(this::executeCommand); - commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); } /** @@ -156,8 +165,10 @@ void show() { */ @FXML private void handleExit() { - GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), - (int) primaryStage.getX(), (int) primaryStage.getY()); + GuiSettings guiSettings = + new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), (int) primaryStage.getX(), + (int) primaryStage.getY() + ); logic.setGuiSettings(guiSettings); helpWindow.hide(); primaryStage.hide(); @@ -170,9 +181,9 @@ public PersonListPanel getPersonListPanel() { /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see Logic#execute(String) */ - private CommandResult executeCommand(String commandText) throws CommandException, ParseException { + private CommandResult executeCommand(String commandText) throws SpleetWaiseCommandException, ParseException { try { CommandResult commandResult = logic.execute(commandText); logger.info("Result: " + commandResult.getFeedbackToUser()); @@ -187,7 +198,7 @@ private CommandResult executeCommand(String commandText) throws CommandException } return commandResult; - } catch (CommandException | ParseException e) { + } catch (SpleetWaiseCommandException | ParseException e) { logger.info("An error occurred while executing command: " + commandText); resultDisplay.setFeedbackToUser(e.getMessage()); throw e; diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/spleetwaise/commons/ui/ResultDisplay.java similarity index 95% rename from src/main/java/seedu/address/ui/ResultDisplay.java rename to src/main/java/spleetwaise/commons/ui/ResultDisplay.java index 7d98e84eedf..ec816c865fe 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/spleetwaise/commons/ui/ResultDisplay.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package spleetwaise.commons.ui; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/spleetwaise/commons/ui/StatusBarFooter.java similarity index 95% rename from src/main/java/seedu/address/ui/StatusBarFooter.java rename to src/main/java/spleetwaise/commons/ui/StatusBarFooter.java index b577f829423..3d53bbd9a9d 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/spleetwaise/commons/ui/StatusBarFooter.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package spleetwaise.commons.ui; import java.nio.file.Path; import java.nio.file.Paths; diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/spleetwaise/commons/ui/Ui.java similarity index 62% rename from src/main/java/seedu/address/ui/Ui.java rename to src/main/java/spleetwaise/commons/ui/Ui.java index 17aa0b494fe..afde99084e3 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/spleetwaise/commons/ui/Ui.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package spleetwaise.commons.ui; import javafx.stage.Stage; @@ -7,7 +7,7 @@ */ public interface Ui { - /** Starts the UI (and the App). */ + /** Starts the UI (and the App). */ void start(Stage primaryStage); } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/spleetwaise/commons/ui/UiManager.java similarity index 74% rename from src/main/java/seedu/address/ui/UiManager.java rename to src/main/java/spleetwaise/commons/ui/UiManager.java index fdf024138bc..dbb0bd26eaa 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/spleetwaise/commons/ui/UiManager.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package spleetwaise.commons.ui; import java.util.logging.Logger; @@ -7,10 +7,10 @@ import javafx.scene.control.Alert.AlertType; import javafx.scene.image.Image; import javafx.stage.Stage; -import seedu.address.MainApp; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; +import spleetwaise.commons.MainApp; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.logic.Logic; +import spleetwaise.commons.util.StringUtil; /** * The manager of the UI component. @@ -22,7 +22,7 @@ public class UiManager implements Ui { private static final Logger logger = LogsCenter.getLogger(UiManager.class); private static final String ICON_APPLICATION = "/images/address_book_32.png"; - private Logic logic; + private final Logic logic; private MainWindow mainWindow; /** @@ -32,6 +32,28 @@ public UiManager(Logic logic) { this.logic = logic; } + /** + * Shows an alert dialog on {@code owner} with the given parameters. This method only returns after the user has + * closed the alert dialog. + */ + private static void showAlertDialogAndWait( + Stage owner, AlertType type, String title, String headerText, + String contentText + ) { + final Alert alert = new Alert(type); + alert.getDialogPane().getStylesheets().add("view/LightTheme.css"); + alert.initOwner(owner); + alert.setTitle(title); + alert.setHeaderText(headerText); + alert.setContentText(contentText); + alert.getDialogPane().setId(ALERT_DIALOG_PANE_FIELD_ID); + alert.showAndWait(); + } + + void showAlertDialogAndWait(Alert.AlertType type, String title, String headerText, String contentText) { + showAlertDialogAndWait(mainWindow.getPrimaryStage(), type, title, headerText, contentText); + } + @Override public void start(Stage primaryStage) { logger.info("Starting UI..."); @@ -50,35 +72,15 @@ public void start(Stage primaryStage) { } } - private Image getImage(String imagePath) { + Image getImage(String imagePath) { return new Image(MainApp.class.getResourceAsStream(imagePath)); } - void showAlertDialogAndWait(Alert.AlertType type, String title, String headerText, String contentText) { - showAlertDialogAndWait(mainWindow.getPrimaryStage(), type, title, headerText, contentText); - } - - /** - * Shows an alert dialog on {@code owner} with the given parameters. - * This method only returns after the user has closed the alert dialog. - */ - private static void showAlertDialogAndWait(Stage owner, AlertType type, String title, String headerText, - String contentText) { - final Alert alert = new Alert(type); - alert.getDialogPane().getStylesheets().add("view/DarkTheme.css"); - alert.initOwner(owner); - alert.setTitle(title); - alert.setHeaderText(headerText); - alert.setContentText(contentText); - alert.getDialogPane().setId(ALERT_DIALOG_PANE_FIELD_ID); - alert.showAndWait(); - } - /** - * Shows an error alert dialog with {@code title} and error message, {@code e}, - * and exits the application after the user has closed the alert dialog. + * Shows an error alert dialog with {@code title} and error message, {@code e}, and exits the application after the + * user has closed the alert dialog. */ - private void showFatalErrorDialogAndShutdown(String title, Throwable e) { + void showFatalErrorDialogAndShutdown(String title, Throwable e) { logger.severe(title + " " + e.getMessage() + StringUtil.getDetails(e)); showAlertDialogAndWait(Alert.AlertType.ERROR, title, e.getMessage(), e.toString()); Platform.exit(); diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/spleetwaise/commons/ui/UiPart.java similarity index 82% rename from src/main/java/seedu/address/ui/UiPart.java rename to src/main/java/spleetwaise/commons/ui/UiPart.java index fc820e01a9c..f58ca8497e9 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/spleetwaise/commons/ui/UiPart.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package spleetwaise.commons.ui; import static java.util.Objects.requireNonNull; @@ -6,11 +6,11 @@ import java.net.URL; import javafx.fxml.FXMLLoader; -import seedu.address.MainApp; +import spleetwaise.commons.MainApp; /** - * Represents a distinct part of the UI. e.g. Windows, dialogs, panels, status bars, etc. - * It contains a scene graph with a root node of type {@code T}. + * Represents a distinct part of the UI. e.g. Windows, dialogs, panels, status bars, etc. It contains a scene graph with + * a root node of type {@code T}. */ public abstract class UiPart { @@ -20,8 +20,8 @@ public abstract class UiPart { private final FXMLLoader fxmlLoader = new FXMLLoader(); /** - * Constructs a UiPart with the specified FXML file URL. - * The FXML file must not specify the {@code fx:controller} attribute. + * Constructs a UiPart with the specified FXML file URL. The FXML file must not specify the {@code fx:controller} + * attribute. */ public UiPart(URL fxmlFileUrl) { loadFxmlFile(fxmlFileUrl, null); @@ -29,6 +29,7 @@ public UiPart(URL fxmlFileUrl) { /** * Constructs a UiPart using the specified FXML file within {@link #FXML_FILE_FOLDER}. + * * @see #UiPart(URL) */ public UiPart(String fxmlFileName) { @@ -36,8 +37,8 @@ public UiPart(String fxmlFileName) { } /** - * Constructs a UiPart with the specified FXML file URL and root object. - * The FXML file must not specify the {@code fx:controller} attribute. + * Constructs a UiPart with the specified FXML file URL and root object. The FXML file must not specify the + * {@code fx:controller} attribute. */ public UiPart(URL fxmlFileUrl, T root) { loadFxmlFile(fxmlFileUrl, root); @@ -45,12 +46,23 @@ public UiPart(URL fxmlFileUrl, T root) { /** * Constructs a UiPart with the specified FXML file within {@link #FXML_FILE_FOLDER} and root object. + * * @see #UiPart(URL, T) */ public UiPart(String fxmlFileName, T root) { this(getFxmlFileUrl(fxmlFileName), root); } + /** + * Returns the FXML file URL for the specified FXML file name within {@link #FXML_FILE_FOLDER}. + */ + private static URL getFxmlFileUrl(String fxmlFileName) { + requireNonNull(fxmlFileName); + String fxmlFileNameWithFolder = FXML_FILE_FOLDER + fxmlFileName; + URL fxmlFileUrl = MainApp.class.getResource(fxmlFileNameWithFolder); + return requireNonNull(fxmlFileUrl); + } + /** * Returns the root object of the scene graph of this UiPart. */ @@ -60,8 +72,9 @@ public T getRoot() { /** * Loads the object hierarchy from a FXML document. + * * @param location Location of the FXML document. - * @param root Specifies the root of the object hierarchy. + * @param root Specifies the root of the object hierarchy. */ private void loadFxmlFile(URL location, T root) { requireNonNull(location); @@ -75,14 +88,4 @@ private void loadFxmlFile(URL location, T root) { } } - /** - * Returns the FXML file URL for the specified FXML file name within {@link #FXML_FILE_FOLDER}. - */ - private static URL getFxmlFileUrl(String fxmlFileName) { - requireNonNull(fxmlFileName); - String fxmlFileNameWithFolder = FXML_FILE_FOLDER + fxmlFileName; - URL fxmlFileUrl = MainApp.class.getResource(fxmlFileNameWithFolder); - return requireNonNull(fxmlFileUrl); - } - } diff --git a/src/main/java/seedu/address/commons/util/AppUtil.java b/src/main/java/spleetwaise/commons/util/AppUtil.java similarity index 94% rename from src/main/java/seedu/address/commons/util/AppUtil.java rename to src/main/java/spleetwaise/commons/util/AppUtil.java index 87aa89c0326..7f25441af6b 100644 --- a/src/main/java/seedu/address/commons/util/AppUtil.java +++ b/src/main/java/spleetwaise/commons/util/AppUtil.java @@ -1,9 +1,9 @@ -package seedu.address.commons.util; +package spleetwaise.commons.util; import static java.util.Objects.requireNonNull; import javafx.scene.image.Image; -import seedu.address.MainApp; +import spleetwaise.commons.MainApp; /** * A container for App specific utility functions diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/spleetwaise/commons/util/CollectionUtil.java similarity index 96% rename from src/main/java/seedu/address/commons/util/CollectionUtil.java rename to src/main/java/spleetwaise/commons/util/CollectionUtil.java index eafe4dfd681..06dc1300359 100644 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ b/src/main/java/spleetwaise/commons/util/CollectionUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package spleetwaise.commons.util; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/commons/util/ConfigUtil.java b/src/main/java/spleetwaise/commons/util/ConfigUtil.java similarity index 77% rename from src/main/java/seedu/address/commons/util/ConfigUtil.java rename to src/main/java/spleetwaise/commons/util/ConfigUtil.java index 7b829c3c4cc..5ce8eba38dd 100644 --- a/src/main/java/seedu/address/commons/util/ConfigUtil.java +++ b/src/main/java/spleetwaise/commons/util/ConfigUtil.java @@ -1,11 +1,11 @@ -package seedu.address.commons.util; +package spleetwaise.commons.util; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.core.Config; -import seedu.address.commons.exceptions.DataLoadingException; +import spleetwaise.commons.core.Config; +import spleetwaise.commons.exceptions.DataLoadingException; /** * A class for accessing the Config File. diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/spleetwaise/commons/util/FileUtil.java similarity index 90% rename from src/main/java/seedu/address/commons/util/FileUtil.java rename to src/main/java/spleetwaise/commons/util/FileUtil.java index b1e2767cdd9..b67748c6f90 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/spleetwaise/commons/util/FileUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package spleetwaise.commons.util; import java.io.IOException; import java.nio.file.Files; @@ -18,8 +18,9 @@ public static boolean isFileExists(Path file) { } /** - * Returns true if {@code path} can be converted into a {@code Path} via {@link Paths#get(String)}, - * otherwise returns false. + * Returns true if {@code path} can be converted into a {@code Path} via {@link Paths#get(String)}, otherwise + * returns false. + * * @param path A string representing the file path. Cannot be null. */ public static boolean isValidPath(String path) { @@ -33,6 +34,7 @@ public static boolean isValidPath(String path) { /** * Creates a file if it does not exist along with its missing parent directories. + * * @throws IOException if the file or directory cannot be created. */ public static void createIfMissing(Path file) throws IOException { @@ -73,8 +75,7 @@ public static String readFromFile(Path file) throws IOException { } /** - * Writes given string to a file. - * Will create the file if it does not exist yet. + * Writes given string to a file. Will create the file if it does not exist yet. */ public static void writeToFile(Path file, String content) throws IOException { Files.write(file, content.getBytes(CHARSET)); diff --git a/src/main/java/spleetwaise/commons/util/IdUtil.java b/src/main/java/spleetwaise/commons/util/IdUtil.java new file mode 100644 index 00000000000..3db82e707f9 --- /dev/null +++ b/src/main/java/spleetwaise/commons/util/IdUtil.java @@ -0,0 +1,52 @@ +package spleetwaise.commons.util; + +import static java.util.Objects.requireNonNull; + +import java.util.UUID; + +/** + * Utility class for generating transaction UUIDs. + */ +public class IdUtil { + + public static final String TEST_ID = "123e4567-e89b-42d3-a456-556642440000"; + public static final String MESSAGE_CONSTRAINTS = "UUID is invalid, refer to " + + "https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/UUID.html."; + private static boolean isDeterminate = false; + + /** + * Sets whether the transaction ID generation is determinate or not. Used for testing purposes, this method allows + * you to control the randomness of the generated IDs. + * + * @param isDeterminate if true, the transaction ID will be deterministic; otherwise, it will be random + */ + public static void setDeterminate(boolean isDeterminate) { + IdUtil.isDeterminate = isDeterminate; + } + + /** + * Generates an UUID. + * + * @return a random UUID. + */ + public static String getId() { + if (isDeterminate) { + return TEST_ID; + } + return UUID.randomUUID().toString(); + } + + /** + * Returns true if a given string is a valid remark (allows empty string ""). + */ + public static boolean isValidId(String id) { + try { + requireNonNull(id); + String trimmedId = id.trim(); + UUID.fromString(trimmedId); + return true; + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/spleetwaise/commons/util/JsonUtil.java similarity index 87% rename from src/main/java/seedu/address/commons/util/JsonUtil.java rename to src/main/java/spleetwaise/commons/util/JsonUtil.java index 100cb16c395..18a6234d8e8 100644 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ b/src/main/java/spleetwaise/commons/util/JsonUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package spleetwaise.commons.util; import static java.util.Objects.requireNonNull; @@ -20,8 +20,8 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataLoadingException; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.exceptions.DataLoadingException; /** * Converts a Java object instance to JSON and vice versa @@ -49,15 +49,16 @@ static T deserializeObjectFromJsonFile(Path jsonFile, Class classOfObject } /** - * Returns the JSON object from the given file or {@code Optional.empty()} object if the file is not found. - * If any values are missing from the file, default values will be used, as long as the file is a valid JSON file. + * Returns the JSON object from the given file or {@code Optional.empty()} object if the file is not found. If any + * values are missing from the file, default values will be used, as long as the file is a valid JSON file. * - * @param filePath cannot be null. + * @param filePath cannot be null. * @param classOfObjectToDeserialize JSON file has to correspond to the structure in the class given here. * @throws DataLoadingException if loading of the JSON file failed. */ public static Optional readJsonFile( - Path filePath, Class classOfObjectToDeserialize) throws DataLoadingException { + Path filePath, Class classOfObjectToDeserialize + ) throws DataLoadingException { requireNonNull(filePath); if (!Files.exists(filePath)) { @@ -78,8 +79,9 @@ public static Optional readJsonFile( } /** - * Saves the Json object to the specified file. - * Overwrites existing file if it exists, creates a new file if it doesn't. + * Saves the Json object to the specified file. Overwrites existing file if it exists, creates a new file if it + * doesn't. + * * @param jsonFile cannot be null * @param filePath cannot be null * @throws IOException if there was an error during writing to the file @@ -94,6 +96,7 @@ public static void saveJsonFile(T jsonFile, Path filePath) throws IOExceptio /** * Converts a given string representation of a JSON data to instance of a class + * * @param The generic type to create an instance of * @return The instance of T with the specified values in the JSON string */ @@ -103,8 +106,9 @@ public static T fromJsonString(String json, Class instanceClass) throws I /** * Converts a given instance of a class into its JSON data string representation + * * @param instance The T object to be converted into the JSON string - * @param The generic type to create an instance of + * @param The generic type to create an instance of * @return JSON data representation of the given class instance, in string */ public static String toJsonString(T instance) throws JsonProcessingException { @@ -129,7 +133,6 @@ protected Level _deserialize(String value, DeserializationContext ctxt) { * Gets the logging level that matches loggingLevelString *

* Returns null if there are no matches - * */ private Level getLoggingLevel(String loggingLevelString) { return Level.parse(loggingLevelString); diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/spleetwaise/commons/util/StringUtil.java similarity index 71% rename from src/main/java/seedu/address/commons/util/StringUtil.java rename to src/main/java/spleetwaise/commons/util/StringUtil.java index 61cc8c9a1cb..e656b727568 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/spleetwaise/commons/util/StringUtil.java @@ -1,7 +1,6 @@ -package seedu.address.commons.util; +package spleetwaise.commons.util; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; import java.io.PrintWriter; import java.io.StringWriter; @@ -13,23 +12,23 @@ public class StringUtil { /** - * Returns true if the {@code sentence} contains the {@code word}. - * Ignores case, but a full word match is required. - *
examples:

+     * Returns true if the {@code sentence} contains the {@code word}. Ignores case, but a full word match is required.
+     * 
examples:
      *       containsWordIgnoreCase("ABc def", "abc") == true
      *       containsWordIgnoreCase("ABc def", "DEF") == true
      *       containsWordIgnoreCase("ABc def", "AB") == false //not a full word match
      *       
+ * * @param sentence cannot be null - * @param word cannot be null, cannot be empty, must be a single word + * @param word cannot be null, cannot be empty, must be a single word */ public static boolean containsWordIgnoreCase(String sentence, String word) { requireNonNull(sentence); requireNonNull(word); String preppedWord = word.trim(); - checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); - checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word"); + AppUtil.checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); + AppUtil.checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word"); String preppedSentence = sentence; String[] wordsInPreppedSentence = preppedSentence.split("\\s+"); @@ -49,10 +48,10 @@ public static String getDetails(Throwable t) { } /** - * Returns true if {@code s} represents a non-zero unsigned integer - * e.g. 1, 2, 3, ..., {@code Integer.MAX_VALUE}
- * Will return false for any other non-null string input - * e.g. empty string, "-1", "0", "+1", and " 2 " (untrimmed), "3 0" (contains whitespace), "1 a" (contains letters) + * Returns true if {@code s} represents a non-zero unsigned integer e.g. 1, 2, 3, ..., {@code Integer.MAX_VALUE} + *
Will return false for any other non-null string input e.g. empty string, "-1", "0", "+1", and " 2 " + * (untrimmed), "3 0" (contains whitespace), "1 a" (contains letters) + * * @throws NullPointerException if {@code s} is null. */ public static boolean isNonZeroUnsignedInteger(String s) { diff --git a/src/main/java/seedu/address/commons/util/ToStringBuilder.java b/src/main/java/spleetwaise/commons/util/ToStringBuilder.java similarity index 91% rename from src/main/java/seedu/address/commons/util/ToStringBuilder.java rename to src/main/java/spleetwaise/commons/util/ToStringBuilder.java index d979b926734..9077499f0e1 100644 --- a/src/main/java/seedu/address/commons/util/ToStringBuilder.java +++ b/src/main/java/spleetwaise/commons/util/ToStringBuilder.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package spleetwaise.commons.util; /** * Builds a string representation of an object that is suitable as the return value of {@link Object#toString()}. @@ -20,8 +20,8 @@ public ToStringBuilder(String objectName) { } /** - * Constructs a {@code ToStringBuilder} whose formatted output will be prefixed with the - * canonical class name of {@code object}. + * Constructs a {@code ToStringBuilder} whose formatted output will be prefixed with the canonical class name of + * {@code object}. */ public ToStringBuilder(Object object) { this(object.getClass().getCanonicalName()); @@ -30,7 +30,7 @@ public ToStringBuilder(Object object) { /** * Adds a field name/value pair to the output string. * - * @param fieldName The name of the field. + * @param fieldName The name of the field. * @param fieldValue The value of the field. * @return A reference to this {@code ToStringBuilder} object, allowing method calls to be chained. */ diff --git a/src/main/java/spleetwaise/transaction/logic/Messages.java b/src/main/java/spleetwaise/transaction/logic/Messages.java new file mode 100644 index 00000000000..ba250eca90b --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/Messages.java @@ -0,0 +1,11 @@ +package spleetwaise.transaction.logic; + +/** + * Container for user visible messages. + */ +public class Messages { + + public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; + public static final String MESSAGE_INVALID_TRANSACTION_DISPLAYED_INDEX = "The transaction index provided is " + + "invalid"; +} diff --git a/src/main/java/spleetwaise/transaction/logic/commands/AddCommand.java b/src/main/java/spleetwaise/transaction/logic/commands/AddCommand.java new file mode 100644 index 00000000000..63e89e1c01d --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/commands/AddCommand.java @@ -0,0 +1,90 @@ +package spleetwaise.transaction.logic.commands; + +import static java.util.Objects.requireNonNull; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_AMOUNT; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DATE; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_STATUS; + +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * This class represents a command for adding transactions. + */ +public class AddCommand extends Command { + + /** + * The word used to trigger this command in the input. + */ + public static final String COMMAND_WORD = "addTxn"; + + /** + * The message that is displayed upon successful execution of this command. + */ + public static final String MESSAGE_SUCCESS = "Transaction added: %s"; + + public static final String MESSAGE_DUPLICATE_TXN = "Transaction already exists in the transaction book"; + + /** + * The message usage string that explains how to use this command. + */ + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Add a new transaction.\n" + + "Parameters: " + "INDEX (must be a positive integer) " + PREFIX_AMOUNT + "AMOUNT " + PREFIX_DESCRIPTION + + "DESCRIPTION " + "[" + PREFIX_DATE + "DATE] " + "[" + PREFIX_STATUS + + "STATUS] " + PREFIX_CATEGORY + "FOOD\n" + "Example: " + COMMAND_WORD + + " 1 " + PREFIX_AMOUNT + "10.00 " + PREFIX_DESCRIPTION + "Paid John for lunch " + PREFIX_DATE + + "23012024 " + PREFIX_CATEGORY + "FOOD " + PREFIX_STATUS + "Not Done"; + + private final Transaction transaction; + + /** + * Creates an AddCommand to add the specified {@code Transaction}. + * + * @param transaction The transaction to be added. + */ + public AddCommand(Transaction transaction) { + requireNonNull(transaction); + this.transaction = transaction; + } + + /** + * This method executes the add command. + * + * @return the result of the execution. + */ + @Override + public CommandResult execute() throws CommandException { + CommonModelManager model = CommonModelManager.getInstance(); + + if (model.hasTransaction(transaction)) { + throw new CommandException(MESSAGE_DUPLICATE_TXN); + } + + model.addTransaction(transaction); + return new CommandResult(String.format(MESSAGE_SUCCESS, transaction)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AddCommand otherAddCommand)) { + return false; + } + + return transaction.equals(otherAddCommand.transaction); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("txnToAdd", transaction).toString(); + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/commands/ClearCommand.java b/src/main/java/spleetwaise/transaction/logic/commands/ClearCommand.java new file mode 100644 index 00000000000..2b31bab8684 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/commands/ClearCommand.java @@ -0,0 +1,22 @@ +package spleetwaise.transaction.logic.commands; + +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.transaction.model.TransactionBook; + +/** + * Represents a command to clear the transaction book. + */ +public class ClearCommand extends Command { + + public static final String COMMAND_WORD = "clearTxn"; + public static final String MESSAGE_SUCCESS = "Transaction book has been cleared!"; + + @Override + public CommandResult execute() { + CommonModelManager model = CommonModelManager.getInstance(); + model.setTransactionBook(new TransactionBook()); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/commands/DeleteCommand.java b/src/main/java/spleetwaise/transaction/logic/commands/DeleteCommand.java new file mode 100644 index 00000000000..a1f60c16c7d --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/commands/DeleteCommand.java @@ -0,0 +1,76 @@ +package spleetwaise.transaction.logic.commands; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Deletes a transaction identified using its displayed index from the transaction book. + */ +public class DeleteCommand extends Command { + + public static final String COMMAND_WORD = "deleteTxn"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the transaction identified by the index number used in the displayed transaction list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_TRANSACTION_SUCCESS = "Deleted Transaction: %1$s"; + + private static final Logger logger = LogsCenter.getLogger(DeleteCommand.class); + + private final Index targetIndex; + + public DeleteCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute() throws CommandException { + logger.log(Level.INFO, "Executing DeleteCommand with index: {0}", targetIndex.getZeroBased()); + + CommonModelManager model = CommonModelManager.getInstance(); + + List lastShownList = model.getFilteredTransactionList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + logger.log(Level.WARNING, "Invalid transaction index: {0}", targetIndex.getZeroBased()); + throw new CommandException("The transaction index provided is invalid"); + } + + Transaction transactionToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteTransaction(transactionToDelete); + logger.log(Level.INFO, "Deleted transaction: {0}", transactionToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_TRANSACTION_SUCCESS, transactionToDelete)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof DeleteCommand otherDeleteCommand)) { + return false; + } + + return targetIndex.equals(otherDeleteCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/commands/EditCommand.java b/src/main/java/spleetwaise/transaction/logic/commands/EditCommand.java new file mode 100644 index 00000000000..2b95df55cfc --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/commands/EditCommand.java @@ -0,0 +1,279 @@ +package spleetwaise.transaction.logic.commands; + +import static java.util.Objects.requireNonNull; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_AMOUNT; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DATE; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DESCRIPTION; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.commons.util.CollectionUtil; +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.logic.Messages; +import spleetwaise.transaction.model.transaction.Amount; +import spleetwaise.transaction.model.transaction.Category; +import spleetwaise.transaction.model.transaction.Date; +import spleetwaise.transaction.model.transaction.Description; +import spleetwaise.transaction.model.transaction.Status; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Edits the details of an existing transaction in the transaction book. + */ +public class EditCommand extends Command { + + public static final String COMMAND_WORD = "editTxn"; + + /** + * The message usage string that explains how to use this command. + */ + public static final String MESSAGE_USAGE = + COMMAND_WORD + ": Edit a transaction.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_PHONE + "CONTACT] " + + "[" + PREFIX_AMOUNT + "AMOUNT] " + + "[" + PREFIX_DESCRIPTION + "DESCRIPTION] " + + "[" + PREFIX_DATE + "DATE] " + + "[" + PREFIX_CATEGORY + "FOOD]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_PHONE + "88888888 " + + PREFIX_AMOUNT + "10.00 " + + PREFIX_DESCRIPTION + "Paid John for lunch " + + PREFIX_DATE + "23012024 " + + PREFIX_CATEGORY + "FOOD " + + PREFIX_CATEGORY + "DRINK "; + + + public static final String MESSAGE_EDIT_TXN_SUCCESS = "Edited Transaction: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_TXN = "This transaction already exists in the transaction book."; + + private final Index index; + private final EditTransactionDescriptor editTransactionDescriptor; + + /** + * @param index of the transaction in the filtered txn list to edit + * @param editTransactionDescriptor details to edit the txn with + */ + public EditCommand(Index index, EditTransactionDescriptor editTransactionDescriptor) { + requireNonNull(index); + requireNonNull(editTransactionDescriptor); + + this.index = index; + this.editTransactionDescriptor = editTransactionDescriptor; + } + + /** + * Creates and returns a {@code Transaction} with the details of {@code transctionToEdit} edited with + * {@code editTransactionDescriptor}. + */ + private static Transaction createEditedTransaction( + Transaction txnToEdit, + EditTransactionDescriptor editTransactionDescriptor + ) { + requireNonNull(txnToEdit); + + String id = editTransactionDescriptor.getId().orElse(txnToEdit.getId()); + Person person = editTransactionDescriptor.getPerson().orElse(txnToEdit.getPerson()); + Amount amount = editTransactionDescriptor.getAmount().orElse(txnToEdit.getAmount()); + Description description = editTransactionDescriptor.getDescription().orElse(txnToEdit.getDescription()); + Date date = editTransactionDescriptor.getDate().orElse(txnToEdit.getDate()); + Status status = editTransactionDescriptor.getStatus().orElse(txnToEdit.getStatus()); + Set categories = editTransactionDescriptor.getCategories().orElse(txnToEdit.getCategories()); + + return new Transaction(id, person, amount, description, date, categories, status); + } + + public EditTransactionDescriptor getDescriptor() { + return editTransactionDescriptor; + } + + @Override + public CommandResult execute() throws CommandException { + CommonModelManager model = CommonModelManager.getInstance(); + List lastShownList = model.getFilteredTransactionList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_TRANSACTION_DISPLAYED_INDEX); + } + + Transaction txnToEdit = lastShownList.get(index.getZeroBased()); + Transaction editedTxn = createEditedTransaction(txnToEdit, editTransactionDescriptor); + + if (!txnToEdit.equals(editedTxn) && model.hasTransaction(editedTxn)) { + throw new CommandException(MESSAGE_DUPLICATE_TXN); + } + + model.setTransaction(txnToEdit, editedTxn); + model.updateFilteredTransactionList(); + return new CommandResult(String.format(MESSAGE_EDIT_TXN_SUCCESS, editedTxn)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof EditCommand otherEditCommand)) { + return false; + } + + return index.equals(otherEditCommand.index) + && editTransactionDescriptor.equals(otherEditCommand.editTransactionDescriptor); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("index", index).add( + "editTransactionDescriptor", + editTransactionDescriptor + ).toString(); + } + + /** + * Represents the data of a transaction that can be edited. + */ + public static class EditTransactionDescriptor { + private String id; + private Person person; + private Amount amount; + private Description description; + private Date date; + private Set categories; + private Status status; + + public EditTransactionDescriptor() { + } + + /** + * Copy constructor. A defensive copy of {@code categories} is used internally. + */ + public EditTransactionDescriptor(EditTransactionDescriptor toCopy) { + setId(toCopy.id); + setPerson(toCopy.person); + setAmount(toCopy.amount); + setDescription(toCopy.description); + setDate(toCopy.date); + setStatus(toCopy.status); + if (toCopy.categories != null) { + setCategories(new HashSet<>(toCopy.categories)); + } + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(person, amount, description, date, categories); + } + + public Optional getId() { + return Optional.ofNullable(id); + } + + public void setId(String id) { + this.id = id; + } + + public Optional getPerson() { + return Optional.ofNullable(person); + } + + public void setPerson(Person person) { + this.person = person; + } + + public Optional getAmount() { + return Optional.ofNullable(amount); + } + + public void setAmount(Amount amount) { + this.amount = amount; + } + + public Optional getDescription() { + return Optional.ofNullable(description); + } + + public void setDescription(Description description) { + this.description = description; + } + + public Optional getDate() { + return Optional.ofNullable(date); + } + + public void setDate(Date date) { + this.date = date; + } + + public Optional getStatus() { + return Optional.ofNullable(status); + } + + public void setStatus(Status status) { + this.status = status; + } + + /** + * Returns an unmodifiable category set, which throws {@code UnsupportedOperationException} if modification is + * attempted. Returns {@code Optional#empty()} if {@code categories} is null. + */ + public Optional> getCategories() { + return (categories != null) ? Optional.of(Collections.unmodifiableSet(categories)) : Optional.empty(); + } + + public void setCategories(Set categories) { + this.categories = new HashSet<>(categories); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditTransactionDescriptor otherEditTransactionDescriptor)) { + return false; + } + + return Objects.equals(id, otherEditTransactionDescriptor.id) + && Objects.equals(person, otherEditTransactionDescriptor.person) + && Objects.equals(amount, otherEditTransactionDescriptor.amount) + && Objects.equals(description, otherEditTransactionDescriptor.description) + && Objects.equals(date, otherEditTransactionDescriptor.date) + && Objects.equals(status, otherEditTransactionDescriptor.status) + && Objects.equals(categories, otherEditTransactionDescriptor.categories); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("id", id) + .add("person", person) + .add("amount", amount) + .add("description", description) + .add("date", date) + .add("status", status) + .add("categories", categories) + .toString(); + } + } + + +} diff --git a/src/main/java/spleetwaise/transaction/logic/commands/FilterCommand.java b/src/main/java/spleetwaise/transaction/logic/commands/FilterCommand.java new file mode 100644 index 00000000000..5e2bc52fbae --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/commands/FilterCommand.java @@ -0,0 +1,90 @@ +package spleetwaise.transaction.logic.commands; + +import static java.util.Objects.requireNonNull; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_AMOUNT; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_AMOUNT_SIGN; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DATE; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_STATUS; + +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.FilterCommandPredicate; + +/** + * This class represents a command for filtering transactions. + */ +public class FilterCommand extends Command { + + /** + * The word used to trigger this command in the input. + */ + public static final String COMMAND_WORD = "filterTxn"; + + /** + * The message that is displayed upon successful execution of this command. + */ + public static final String MESSAGE_SUCCESS = "Transaction book filtered."; + + /** + * The message usage string that explains how to use this command. + */ + public static final String MESSAGE_USAGE = + COMMAND_WORD + ": Filter the transaction book.\n" + "Parameters: " + + "[INDEX] " + "[" + PREFIX_AMOUNT + "AMOUNT] " + "[" + PREFIX_DESCRIPTION + + "DESCRIPTION] " + "[" + PREFIX_DATE + "DATE] " + "[" + PREFIX_STATUS + + "STATUS] " + "[" + PREFIX_AMOUNT_SIGN + "AMOUNT_SIGN] " + + "[" + PREFIX_CATEGORY + "CATEGORY]\n" + + "At least one of the above filtering criteria is needed\n" + + "Example: " + COMMAND_WORD + " 1"; + + private final FilterCommandPredicate filterPredicate; + + /** + * Creates an AddCommand to add the specified {@code Transaction}. + * + * @param predicate the predicate to filter the transactions. + */ + public FilterCommand(FilterCommandPredicate predicate) { + requireNonNull(predicate); + + filterPredicate = predicate; + } + + /** + * This method executes the filter command. + * + * @return the result of the execution. + */ + @Override + public CommandResult execute() throws CommandException { + CommonModelManager model = CommonModelManager.getInstance(); + + model.updateFilteredTransactionList(filterPredicate); + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof FilterCommand otherFilterCommand)) { + return false; + } + + return filterPredicate.equals(otherFilterCommand.filterPredicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", filterPredicate) + .toString(); + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/commands/ListCommand.java b/src/main/java/spleetwaise/transaction/logic/commands/ListCommand.java new file mode 100644 index 00000000000..bd3e4f41f91 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/commands/ListCommand.java @@ -0,0 +1,25 @@ +package spleetwaise.transaction.logic.commands; + +import static spleetwaise.transaction.model.TransactionBookModelManager.PREDICATE_SHOW_ALL_TXNS; + +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.model.CommonModelManager; + +/** + * Lists all transactions in the transaction book to the user. + */ +public class ListCommand extends Command { + + public static final String COMMAND_WORD = "listTxn"; + + public static final String MESSAGE_SUCCESS = "Listed all transactions"; + + + @Override + public CommandResult execute() { + CommonModelManager model = CommonModelManager.getInstance(); + model.updateFilteredTransactionList(PREDICATE_SHOW_ALL_TXNS); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/commands/MarkCommand.java b/src/main/java/spleetwaise/transaction/logic/commands/MarkCommand.java new file mode 100644 index 00000000000..ae0dd3141f6 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/commands/MarkCommand.java @@ -0,0 +1,54 @@ +package spleetwaise.transaction.logic.commands; + +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Status; + +/** + * Marks a transaction identified using its displayed index from the transaction book. + */ +public class MarkCommand extends UpdateStatusCommand { + + public static final String COMMAND_WORD = "markDone"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Marks the transaction as Done identified by the index number used in the displayed transaction list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_MARK_TRANSACTION_SUCCESS = "Marked Transaction as Done: %1$s"; + + public MarkCommand(Index targetIndex) { + super(targetIndex); + } + + @Override + protected Status getUpdatedStatus() { + return new Status(true); // Marked as done + } + + @Override + protected String getSuccessMessage() { + return MESSAGE_MARK_TRANSACTION_SUCCESS; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof MarkCommand otherMarkCommand)) { + return false; + } + + return targetIndex.equals(otherMarkCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/commands/UnmarkCommand.java b/src/main/java/spleetwaise/transaction/logic/commands/UnmarkCommand.java new file mode 100644 index 00000000000..8bdeb94f6a8 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/commands/UnmarkCommand.java @@ -0,0 +1,56 @@ +package spleetwaise.transaction.logic.commands; + +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Status; + +/** + * Unmarks a transaction identified using its displayed index from the transaction book. + */ +public class UnmarkCommand extends UpdateStatusCommand { + + public static final String COMMAND_WORD = "markUndone"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Marks the transaction as undone identified by the index number used in the displayed transaction " + + "list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_UNMARK_TRANSACTION_SUCCESS = "Marked Transaction as Undone: %1$s"; + + public UnmarkCommand(Index targetIndex) { + super(targetIndex); + } + + @Override + protected Status getUpdatedStatus() { + return new Status(false); // Marked as undone + } + + @Override + protected String getSuccessMessage() { + return MESSAGE_UNMARK_TRANSACTION_SUCCESS; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof UnmarkCommand otherUnmarkCommand)) { + return false; + } + + return targetIndex.equals(otherUnmarkCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} + diff --git a/src/main/java/spleetwaise/transaction/logic/commands/UpdateStatusCommand.java b/src/main/java/spleetwaise/transaction/logic/commands/UpdateStatusCommand.java new file mode 100644 index 00000000000..8e8c3b246ac --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/commands/UpdateStatusCommand.java @@ -0,0 +1,65 @@ +package spleetwaise.transaction.logic.commands; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.commands.CommandResult; +import spleetwaise.commons.logic.commands.exceptions.CommandException; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.transaction.logic.Messages; +import spleetwaise.transaction.model.transaction.Status; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Abstract command to update the status of a transaction. + */ +public abstract class UpdateStatusCommand extends Command { + + private static final Logger logger = LogsCenter.getLogger(MarkCommand.class); + + protected final Index targetIndex; + + public UpdateStatusCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + /** + * Subclasses must provide the new status. + */ + protected abstract Status getUpdatedStatus(); + + /** + * Subclasses must provide the success message. + */ + protected abstract String getSuccessMessage(); + + @Override + public CommandResult execute() throws CommandException { + logger.log( + Level.INFO, "Executing {0} with index: {1}", + new Object[]{ this.getClass().getSimpleName(), targetIndex.getZeroBased() } + ); + + CommonModelManager model = CommonModelManager.getInstance(); + + List lastShownList = model.getFilteredTransactionList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + logger.log(Level.WARNING, "Invalid transaction index: {0}", targetIndex.getZeroBased()); + throw new CommandException(Messages.MESSAGE_INVALID_TRANSACTION_DISPLAYED_INDEX); + } + + Transaction txnToUpdate = lastShownList.get(targetIndex.getZeroBased()); + Transaction updatedTxn = txnToUpdate.setStatus(getUpdatedStatus()); + + model.setTransaction(txnToUpdate, updatedTxn); + model.updateFilteredTransactionList(); + + logger.log(Level.INFO, "Updated transaction: {0}", updatedTxn); + return new CommandResult(String.format(getSuccessMessage(), updatedTxn)); + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/parser/AddCommandParser.java b/src/main/java/spleetwaise/transaction/logic/parser/AddCommandParser.java new file mode 100644 index 00000000000..4bc7aeb3231 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/parser/AddCommandParser.java @@ -0,0 +1,69 @@ +package spleetwaise.transaction.logic.parser; + +import static spleetwaise.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static spleetwaise.transaction.logic.commands.AddCommand.MESSAGE_USAGE; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_AMOUNT; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DATE; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_STATUS; +import static spleetwaise.transaction.model.transaction.Date.getNowDate; +import static spleetwaise.transaction.model.transaction.Status.NOT_DONE_STATUS; + +import java.util.Set; + +import spleetwaise.address.logic.parser.ArgumentMultimap; +import spleetwaise.address.logic.parser.ArgumentTokenizer; +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.parser.Parser; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.transaction.logic.commands.AddCommand; +import spleetwaise.transaction.model.transaction.Amount; +import spleetwaise.transaction.model.transaction.Category; +import spleetwaise.transaction.model.transaction.Date; +import spleetwaise.transaction.model.transaction.Description; +import spleetwaise.transaction.model.transaction.Status; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Parses input arguments and creates a new transaction AddCommand object. + */ +public class AddCommandParser implements Parser { + /** + * Parses the given {@code String} argument in the context of the transaction AddCommand and returns an AddCommand + * object for execution. + * + * @param args The string argument to be parsed. + * @return The AddCommand object to execute. + * @throws ParseException string argument contains invalid arguments. + */ + public AddCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize( + args, PREFIX_AMOUNT, PREFIX_DESCRIPTION, PREFIX_DATE, PREFIX_CATEGORY, PREFIX_STATUS); + + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException e) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE), e); + } + + if (!ParserUtil.arePrefixesPresent(argMultimap, PREFIX_AMOUNT, PREFIX_DESCRIPTION)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE)); + } + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_AMOUNT, PREFIX_DESCRIPTION, PREFIX_DATE, PREFIX_STATUS); + + Person person = ParserUtil.getPersonByFilteredPersonListIndex(index); + Amount amount = ParserUtil.parseAmount(argMultimap.getValue(PREFIX_AMOUNT).get()); + Description description = ParserUtil.parseDescription(argMultimap.getValue(PREFIX_DESCRIPTION).get()); + Date date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).orElse(getNowDate())); + Set categories = ParserUtil.parseCategories(argMultimap.getAllValues(PREFIX_CATEGORY)); + Status status = ParserUtil.parseStatus(argMultimap.getValue(PREFIX_STATUS).orElse(NOT_DONE_STATUS)); + + Transaction transaction = new Transaction(person, amount, description, date, categories, status); + + return new AddCommand(transaction); + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/parser/CliSyntax.java b/src/main/java/spleetwaise/transaction/logic/parser/CliSyntax.java new file mode 100644 index 00000000000..112a0dc102e --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/parser/CliSyntax.java @@ -0,0 +1,17 @@ +package spleetwaise.transaction.logic.parser; + +import spleetwaise.address.logic.parser.Prefix; + +/** + * Contains Command Line Interface (CLI) syntax definitions common to multiple commands + */ +public class CliSyntax { + + /* Prefix definitions for transaction commands */ + public static final Prefix PREFIX_AMOUNT = new Prefix("amt/"); + public static final Prefix PREFIX_DESCRIPTION = new Prefix("desc/"); + public static final Prefix PREFIX_DATE = new Prefix("date/"); + public static final Prefix PREFIX_CATEGORY = new Prefix("cat/"); + public static final Prefix PREFIX_STATUS = new Prefix("status/"); + public static final Prefix PREFIX_AMOUNT_SIGN = new Prefix("amtsign/"); +} diff --git a/src/main/java/spleetwaise/transaction/logic/parser/DeleteCommandParser.java b/src/main/java/spleetwaise/transaction/logic/parser/DeleteCommandParser.java new file mode 100644 index 00000000000..9a4e8e40bfe --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/parser/DeleteCommandParser.java @@ -0,0 +1,30 @@ +package spleetwaise.transaction.logic.parser; + +import static spleetwaise.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.parser.Parser; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.transaction.logic.commands.DeleteCommand; + +/** + * Parses input arguments and creates a new DeleteCommand object + */ +public class DeleteCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteCommand and returns a DeleteCommand + * object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/parser/EditCommandParser.java b/src/main/java/spleetwaise/transaction/logic/parser/EditCommandParser.java new file mode 100644 index 00000000000..ec5546bca34 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/parser/EditCommandParser.java @@ -0,0 +1,104 @@ +package spleetwaise.transaction.logic.parser; + +import static spleetwaise.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static spleetwaise.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static spleetwaise.address.logic.parser.ParserUtil.parsePhone; +import static spleetwaise.transaction.logic.commands.EditCommand.MESSAGE_USAGE; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_AMOUNT; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DATE; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DESCRIPTION; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import spleetwaise.address.logic.parser.ArgumentMultimap; +import spleetwaise.address.logic.parser.ArgumentTokenizer; +import spleetwaise.address.model.person.Phone; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.parser.Parser; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.transaction.logic.commands.EditCommand; +import spleetwaise.transaction.logic.commands.EditCommand.EditTransactionDescriptor; +import spleetwaise.transaction.model.transaction.Amount; +import spleetwaise.transaction.model.transaction.Category; +import spleetwaise.transaction.model.transaction.Date; +import spleetwaise.transaction.model.transaction.Description; + + +/** + * Parses input arguments and creates a new EditCommand object + */ +public class EditCommandParser implements Parser { + @Override + public EditCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize( + args, PREFIX_PHONE, PREFIX_AMOUNT, PREFIX_DESCRIPTION, PREFIX_DATE, PREFIX_CATEGORY); + + // Parse index + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException e) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE), e); + } + + // Parse descriptors + argMultimap.verifyNoDuplicatePrefixesFor( + PREFIX_PHONE, PREFIX_AMOUNT, PREFIX_DESCRIPTION, PREFIX_DATE); + + EditTransactionDescriptor editTransactionDescriptor = new EditTransactionDescriptor(); + + if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { + Phone phone = parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); + editTransactionDescriptor.setPerson(ParserUtil.getPersonFromPhone(phone)); + } + + if (argMultimap.getValue(PREFIX_AMOUNT).isPresent()) { + Amount amount = ParserUtil.parseAmount(argMultimap.getValue(PREFIX_AMOUNT).get()); + editTransactionDescriptor.setAmount(amount); + } + + if (argMultimap.getValue(PREFIX_DESCRIPTION).isPresent()) { + Description desc = ParserUtil.parseDescription(argMultimap.getValue(PREFIX_DESCRIPTION).get()); + editTransactionDescriptor.setDescription(desc); + } + + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + Date date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + editTransactionDescriptor.setDate(date); + } + + parseCategoriesForEdit(argMultimap.getAllValues(PREFIX_CATEGORY)).ifPresent( + editTransactionDescriptor::setCategories); + + // Check if any fields edited + if (!editTransactionDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + } + + return new EditCommand(index, editTransactionDescriptor); + + } + + /** + * Parses {@code Collection categories} into a {@code Set} if {@code categories} is non-empty. If + * {@code categories} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero categories. + */ + private Optional> parseCategoriesForEdit(Collection categories) throws ParseException { + assert categories != null; + + if (categories.isEmpty()) { + return Optional.empty(); + } + Collection categorySet = categories.size() == 1 && categories.contains("") ? Collections.emptySet() + : categories; + return Optional.of(ParserUtil.parseCategories(categorySet)); + } + + +} diff --git a/src/main/java/spleetwaise/transaction/logic/parser/FilterCommandParser.java b/src/main/java/spleetwaise/transaction/logic/parser/FilterCommandParser.java new file mode 100644 index 00000000000..c4b0013d558 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/parser/FilterCommandParser.java @@ -0,0 +1,167 @@ +package spleetwaise.transaction.logic.parser; + +import static spleetwaise.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static spleetwaise.transaction.logic.commands.FilterCommand.MESSAGE_USAGE; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_AMOUNT; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_AMOUNT_SIGN; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DATE; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_STATUS; + +import java.util.ArrayList; +import java.util.function.Predicate; + +import spleetwaise.address.logic.parser.ArgumentMultimap; +import spleetwaise.address.logic.parser.ArgumentTokenizer; +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.parser.Parser; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.transaction.logic.commands.FilterCommand; +import spleetwaise.transaction.model.FilterCommandPredicate; +import spleetwaise.transaction.model.filterpredicate.AmountFilterPredicate; +import spleetwaise.transaction.model.filterpredicate.AmountSignFilterPredicate; +import spleetwaise.transaction.model.filterpredicate.CategoryFilterPredicate; +import spleetwaise.transaction.model.filterpredicate.DateFilterPredicate; +import spleetwaise.transaction.model.filterpredicate.DescriptionFilterPredicate; +import spleetwaise.transaction.model.filterpredicate.PersonFilterPredicate; +import spleetwaise.transaction.model.filterpredicate.StatusFilterPredicate; +import spleetwaise.transaction.model.transaction.Amount; +import spleetwaise.transaction.model.transaction.Category; +import spleetwaise.transaction.model.transaction.Date; +import spleetwaise.transaction.model.transaction.Description; +import spleetwaise.transaction.model.transaction.Status; +import spleetwaise.transaction.model.transaction.Transaction; + +//@@author rollingpencil +/** + * Parses input arguments and creates a new transaction FilterCommand object. + */ +public class FilterCommandParser implements Parser { + private static void parsePersonFilter( + ArgumentMultimap argMultimap, + ArrayList> filterSubPredicates + ) + throws ParseException { + if (!argMultimap.getPreamble().isEmpty()) { + Index index = ParserUtil.parseIndex(argMultimap.getPreamble()); + Person person = ParserUtil.getPersonByFilteredPersonListIndex(index); + filterSubPredicates.add(new PersonFilterPredicate(person)); + } + } + + private static void parseAmountFilter( + ArgumentMultimap argMultimap, + ArrayList> filterSubPredicates + ) + throws ParseException { + if (argMultimap.getValue(PREFIX_AMOUNT).isPresent()) { + Amount amount = ParserUtil.parseAmount(argMultimap.getValue(PREFIX_AMOUNT).get()); + filterSubPredicates.add(new AmountFilterPredicate(amount)); + } + } + + private static void parseDescriptionFilter( + ArgumentMultimap argMultimap, + ArrayList> filterSubPredicates + ) + throws ParseException { + if (argMultimap.getValue(PREFIX_DESCRIPTION).isPresent()) { + Description description = ParserUtil.parseDescription(argMultimap.getValue(PREFIX_DESCRIPTION).get()); + filterSubPredicates.add(new DescriptionFilterPredicate(description)); + } + } + + private static void parseDateFilter( + ArgumentMultimap argMultimap, + ArrayList> filterSubPredicates + ) throws ParseException { + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + Date date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + filterSubPredicates.add(new DateFilterPredicate(date)); + } + } + + private static void parseStatusFilter( + ArgumentMultimap argMultimap, + ArrayList> filterSubPredicates + ) + throws ParseException { + if (argMultimap.getValue(PREFIX_STATUS).isPresent()) { + Status status = ParserUtil.parseStatus(argMultimap.getValue(PREFIX_STATUS).get()); + filterSubPredicates.add(new StatusFilterPredicate(status)); + } + } + + private static void parseAmountSignFilter( + ArgumentMultimap argMultimap, + ArrayList> filterSubPredicates + ) + throws ParseException { + if (argMultimap.getValue(PREFIX_AMOUNT_SIGN).isPresent()) { + AmountSignFilterPredicate amtSignPred = ParserUtil.parseAmountSign( + argMultimap.getValue(PREFIX_AMOUNT_SIGN).get()); + filterSubPredicates.add(amtSignPred); + } + } + + private static void parseCategoryFilter( + ArgumentMultimap argMultimap, + ArrayList> filterSubPredicates + ) + throws ParseException { + if (argMultimap.getValue(PREFIX_CATEGORY).isPresent()) { + Category category = ParserUtil.parseCategory(argMultimap.getValue(PREFIX_CATEGORY).get()); + filterSubPredicates.add(new CategoryFilterPredicate(category)); + } + } + + /** + * Parses the given {@code String} argument in the context of the transaction FilterCommand and returns an + * FilterCommand object for execution. + * + * @param args The string argument to be parsed. + * @return The FilterCommand object to execute. + * @throws ParseException string argument contains invalid arguments. + */ + public FilterCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_AMOUNT, PREFIX_DESCRIPTION, PREFIX_DATE, PREFIX_STATUS, + PREFIX_AMOUNT_SIGN, PREFIX_CATEGORY + ); + if (!ParserUtil.areAnyPrefixesPresent(argMultimap, PREFIX_AMOUNT, PREFIX_DESCRIPTION, PREFIX_DATE, + PREFIX_STATUS, + PREFIX_AMOUNT_SIGN, + PREFIX_CATEGORY + ) + && argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE)); + } + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_AMOUNT, PREFIX_DESCRIPTION, PREFIX_DATE, PREFIX_STATUS, + PREFIX_AMOUNT_SIGN, PREFIX_CATEGORY + ); + + ArrayList> filterSubPredicates = new ArrayList<>(); + + parsePersonFilter(argMultimap, filterSubPredicates); + + parseAmountFilter(argMultimap, filterSubPredicates); + + parseDescriptionFilter(argMultimap, filterSubPredicates); + + parseDateFilter(argMultimap, filterSubPredicates); + + parseStatusFilter(argMultimap, filterSubPredicates); + + parseAmountSignFilter(argMultimap, filterSubPredicates); + + parseCategoryFilter(argMultimap, filterSubPredicates); + + assert !filterSubPredicates.isEmpty(); + + FilterCommandPredicate filterPredicate = new FilterCommandPredicate(filterSubPredicates); + + return new FilterCommand(filterPredicate); + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/parser/MarkCommandParser.java b/src/main/java/spleetwaise/transaction/logic/parser/MarkCommandParser.java new file mode 100644 index 00000000000..661bb92f22c --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/parser/MarkCommandParser.java @@ -0,0 +1,30 @@ +package spleetwaise.transaction.logic.parser; + +import static spleetwaise.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.parser.Parser; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.transaction.logic.commands.MarkCommand; + +/** + * Parses input arguments and creates a new MarkCommand object + */ +public class MarkCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the MarkCommand and returns a MarkCommand object + * for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public MarkCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new MarkCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MarkCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/parser/ParserUtil.java b/src/main/java/spleetwaise/transaction/logic/parser/ParserUtil.java new file mode 100644 index 00000000000..aa51196aa9b --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/parser/ParserUtil.java @@ -0,0 +1,162 @@ +package spleetwaise.transaction.logic.parser; + +import static java.util.Objects.requireNonNull; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import spleetwaise.address.logic.Messages; +import spleetwaise.address.model.person.Person; +import spleetwaise.address.model.person.Phone; +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.parser.BaseParserUtil; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.transaction.model.filterpredicate.AmountSignFilterPredicate; +import spleetwaise.transaction.model.transaction.Amount; +import spleetwaise.transaction.model.transaction.Category; +import spleetwaise.transaction.model.transaction.Date; +import spleetwaise.transaction.model.transaction.Description; +import spleetwaise.transaction.model.transaction.Status; + +/** + * Contains utility methods used for parsing strings in the various *Parser classes. + */ +public class ParserUtil extends BaseParserUtil { + public static final String MESSAGE_PHONE_NUMBER_IS_UNKNOWN = "No matching phone number found. Please check if the " + + "contact exists in your address book."; + + /** + * Parses a {@code String amount} into a {@code Amount}. Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code amount} is invalid. + */ + public static Amount parseAmount(String amount) throws ParseException { + requireNonNull(amount); + amount = amount.trim(); + if (!Amount.isValidAmount(amount)) { + throw new ParseException(Amount.MESSAGE_CONSTRAINTS); + } + return new Amount(amount); + } + + /** + * Parses a {@code String description} into a {@code Description}. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the given {@code description} is invalid. + */ + public static Description parseDescription(String description) throws ParseException { + requireNonNull(description); + description = description.trim(); + if (!Description.isValidDescription(description)) { + throw new ParseException(Description.MESSAGE_CONSTRAINTS); + } + return new Description(description); + } + + /** + * Parses a {@code String date} into a {@code Date}. Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code date} is invalid. + */ + public static Date parseDate(String date) throws ParseException { + requireNonNull(date); + date = date.trim(); + if (!Date.isValidDate(date)) { + throw new ParseException(Date.MESSAGE_CONSTRAINTS); + } + return new Date(date); + } + + /** + * Parses a {@code String status} into a {@code Status}. Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code status} is invalid. + */ + public static Status parseStatus(String status) throws ParseException { + requireNonNull(status); + status = status.trim(); + if (!Status.isValidStatus(status)) { + throw new ParseException(Status.MESSAGE_CONSTRAINTS); + } + return new Status(status); + } + + /** + * Parses a {@code String catStr} into a {@code Category} that represents the category. Leading and trailing + * whitespaces will be trimmed and capitalized + */ + public static Category parseCategory(String catStr) throws ParseException { + requireNonNull(catStr); + String trimmedCategory = catStr.trim().toUpperCase(); + if (!Category.isValidCatName(trimmedCategory)) { + throw new ParseException(Category.MESSAGE_CONSTRAINTS); + } + return new Category(trimmedCategory); + } + + /** + * Parses {@code Collection Category} into a {@code Set}. + */ + public static Set parseCategories(Collection categoryStrs) throws ParseException { + requireNonNull(categoryStrs); + final Set categories = new HashSet<>(); + for (String categoryStr : categoryStrs) { + categories.add(parseCategory(categoryStr)); + } + return categories; + } + + /** + * Finds the corresponding Person who has the provided phone number. + * + * @param phone The phone to search using. + * @return A Person who has the specified phone. + * @throws ParseException No person was found with the phone. + */ + public static Person getPersonFromPhone(Phone phone) throws ParseException { + requireNonNull(phone); + Optional p = CommonModelManager.getInstance().getPersonByPhone(phone); + if (p.isEmpty()) { + throw new ParseException(MESSAGE_PHONE_NUMBER_IS_UNKNOWN); + } + return p.get(); + } + + /** + * Retrieves a {@code Person} from the filtered person list at the specified {@code Index}. + * + * @param index The {@code Index} of the person to retrieve from the filtered address book list. + * @return The {@code Person} at the specified index in the filtered person list. + * @throws ParseException If the specified index is out of bounds for the filtered person list. + */ + public static Person getPersonByFilteredPersonListIndex(Index index) + throws ParseException { + requireNonNull(index); + List lastShownList = CommonModelManager.getInstance().getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new ParseException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + return lastShownList.get(index.getZeroBased()); + } + + /** + * Parses a {@code String amountSign} into a {@code AmountSignFilterPredicate}. + * + * @throws ParseException if the given {@code amountSign} is invalid. + */ + public static AmountSignFilterPredicate parseAmountSign(String amountSign) throws ParseException { + requireNonNull(amountSign); + amountSign = amountSign.trim(); + if (!AmountSignFilterPredicate.isValidSign(amountSign)) { + throw new ParseException(AmountSignFilterPredicate.MESSAGE_CONSTRAINTS); + } + return new AmountSignFilterPredicate(amountSign); + } +} diff --git a/src/main/java/spleetwaise/transaction/logic/parser/TransactionBookParser.java b/src/main/java/spleetwaise/transaction/logic/parser/TransactionBookParser.java new file mode 100644 index 00000000000..435cc49ada1 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/parser/TransactionBookParser.java @@ -0,0 +1,74 @@ +package spleetwaise.transaction.logic.parser; + +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.logic.commands.Command; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.transaction.logic.commands.AddCommand; +import spleetwaise.transaction.logic.commands.ClearCommand; +import spleetwaise.transaction.logic.commands.DeleteCommand; +import spleetwaise.transaction.logic.commands.EditCommand; +import spleetwaise.transaction.logic.commands.FilterCommand; +import spleetwaise.transaction.logic.commands.ListCommand; +import spleetwaise.transaction.logic.commands.MarkCommand; +import spleetwaise.transaction.logic.commands.UnmarkCommand; + +/** + * Parses user input. + */ +public class TransactionBookParser { + + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = + Pattern.compile("(?\\S+)(?.*)"); + private static final Logger logger = LogsCenter.getLogger(TransactionBookParser.class); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + return null; + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + + // Note to developers: Change the log level in config.json to enable lower level (i.e., FINE, FINER and lower) + // log messages such as the one below. + // Lower level log messages are used sparingly to minimize noise in the code. + logger.fine("Command word: " + commandWord + "; Arguments: " + arguments); + + switch (commandWord) { + case AddCommand.COMMAND_WORD: + return new AddCommandParser().parse(arguments); + case ListCommand.COMMAND_WORD: + return new ListCommand(); + case ClearCommand.COMMAND_WORD: + return new ClearCommand(); + case FilterCommand.COMMAND_WORD: + return new FilterCommandParser().parse(arguments); + case DeleteCommand.COMMAND_WORD: + return new DeleteCommandParser().parse(arguments); + case EditCommand.COMMAND_WORD: + return new EditCommandParser().parse(arguments); + case MarkCommand.COMMAND_WORD: + return new MarkCommandParser().parse(arguments); + case UnmarkCommand.COMMAND_WORD: + return new UnmarkCommandParser().parse(arguments); + default: + return null; + } + } + +} diff --git a/src/main/java/spleetwaise/transaction/logic/parser/UnmarkCommandParser.java b/src/main/java/spleetwaise/transaction/logic/parser/UnmarkCommandParser.java new file mode 100644 index 00000000000..7e8f1d6e293 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/logic/parser/UnmarkCommandParser.java @@ -0,0 +1,31 @@ +package spleetwaise.transaction.logic.parser; + +import static spleetwaise.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import spleetwaise.commons.core.index.Index; +import spleetwaise.commons.logic.parser.Parser; +import spleetwaise.commons.logic.parser.exceptions.ParseException; +import spleetwaise.transaction.logic.commands.UnmarkCommand; + +/** + * Parses input arguments and creates a new UnmarkCommand object + */ +public class UnmarkCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the UnmarkCommand and returns a UnmarkCommand + * object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public UnmarkCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new UnmarkCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, UnmarkCommand.MESSAGE_USAGE), pe); + } + } +} + diff --git a/src/main/java/spleetwaise/transaction/model/FilterCommandPredicate.java b/src/main/java/spleetwaise/transaction/model/FilterCommandPredicate.java new file mode 100644 index 00000000000..87bd8b68f91 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/FilterCommandPredicate.java @@ -0,0 +1,61 @@ +package spleetwaise.transaction.model; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.function.Predicate; + +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Tests that a {@code Transaction} matches any of the condition given. + */ +public class FilterCommandPredicate implements Predicate { + + private final ArrayList> filterSubPredicates; + + + /** + * Constructs a {@code FilterCommandPredicate} with the given list of sub-predicates. + * + * @param filterSubPredicates The list of sub-predicates to filter the transaction by. + * Must contain at least 1 sub-predicate. + */ + public FilterCommandPredicate(ArrayList> filterSubPredicates) { + requireNonNull(filterSubPredicates); + if (filterSubPredicates.isEmpty()) { + throw new NullPointerException(); + } + + this.filterSubPredicates = filterSubPredicates; + } + + @Override + public boolean test(Transaction txn) { + return filterSubPredicates.stream().reduce(pred -> true, Predicate::and).test(txn); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof FilterCommandPredicate otherFilterCommandPredicate)) { + return false; + } + + return filterSubPredicates.containsAll(otherFilterCommandPredicate.filterSubPredicates) + && otherFilterCommandPredicate.filterSubPredicates.containsAll(filterSubPredicates); + } + + @Override + public String toString() { + ToStringBuilder sb = new ToStringBuilder(this); + for (int i = 0; i < filterSubPredicates.size(); i++) { + sb.add("pred" + i, filterSubPredicates.get(i)); + } + return sb.toString(); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/ReadOnlyTransactionBook.java b/src/main/java/spleetwaise/transaction/model/ReadOnlyTransactionBook.java new file mode 100644 index 00000000000..7bf8d6f8ac7 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/ReadOnlyTransactionBook.java @@ -0,0 +1,14 @@ +package spleetwaise.transaction.model; + +import javafx.collections.ObservableList; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Unmodifiable view of an transaction book. + */ +public interface ReadOnlyTransactionBook { + /** + * Returns an unmodifiable view of the transaction book. + */ + ObservableList getTransactionList(); +} diff --git a/src/main/java/spleetwaise/transaction/model/TransactionBook.java b/src/main/java/spleetwaise/transaction/model/TransactionBook.java new file mode 100644 index 00000000000..500bc1f979b --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/TransactionBook.java @@ -0,0 +1,162 @@ +package spleetwaise.transaction.model; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import spleetwaise.commons.util.CollectionUtil; +import spleetwaise.transaction.model.transaction.Transaction; +import spleetwaise.transaction.model.transaction.exceptions.DuplicateTransactionException; +import spleetwaise.transaction.model.transaction.exceptions.TransactionNotFoundException; + +/** + * Wraps all data at the transaction book level. + */ +public class TransactionBook implements ReadOnlyTransactionBook { + + private final ObservableList transactionList; + private final ObservableList transactionUnmodifiableList; + + /** + * Creates an TransactionBook using the Transactions in the {@code existingTransactionList}. + */ + public TransactionBook(List existingTransactionList) { + transactionList = FXCollections.observableArrayList(existingTransactionList); + transactionUnmodifiableList = FXCollections.unmodifiableObservableList(transactionList); + } + + public TransactionBook() { + this(new ArrayList<>()); + } + + /** + * Creates a TransactionBook using the Transactions in the {@code existingTransactionBook}. + */ + public TransactionBook(TransactionBook existingTransactionBook) { + this(existingTransactionBook.transactionList); + } + + /** + * Creates a TransactionBook using the Transactions in the {@code toBeCopied} + */ + public TransactionBook(ReadOnlyTransactionBook toBeCopied) { + this(); + ObservableList txns = toBeCopied.getTransactionList(); + + CollectionUtil.requireAllNonNull(txns); + transactionList.setAll(txns); + } + + /** + * Checks if the provided transaction already has an entry in the transaction book. + * + * @param transaction The transaction to check against. + * @return true if transaction exists in the book. + */ + public boolean containsTransaction(Transaction transaction) { + return transactionList.contains(transaction); + } + + /** + * Returns true if the list contains a transaction with a matching id + */ + public boolean containsTransactionById(Transaction toCheck) { + requireNonNull(toCheck); + return transactionList.stream().anyMatch(toCheck::hasSameId); + } + + /** + * Replaces the existing data in the transaction list with the data in the replacement. + * + * @param replacementTransactionBook The data used to replace the current transactions. + */ + public void setTransactionBook(ReadOnlyTransactionBook replacementTransactionBook) { + requireNonNull(replacementTransactionBook); + transactionList.clear(); + transactionList.addAll(replacementTransactionBook.getTransactionList()); + } + + /** + * Replaces the transaction {@code target} in the list with {@code replacement}. {@code target} must exist in the + * list. The txn identity of {@code replacement} must not be the same as another existing txn in the list. + */ + public void setTransaction(Transaction target, Transaction replacement) { + CollectionUtil.requireAllNonNull(target, replacement); + int i = transactionList.indexOf(target); + if (i == -1) { + throw new TransactionNotFoundException(); + } + + if (!target.equals(replacement) && containsTransaction(replacement)) { + throw new DuplicateTransactionException(); + } + + transactionList.set(i, replacement); + } + + /** + * Adds a transaction entry to the transaction list. + * + * @param transaction The transaction to be added. + * @throws DuplicateTransactionException If the transaction already exists. + */ + public void addTransaction(Transaction transaction) throws DuplicateTransactionException { + requireNonNull(transaction); + if (containsTransaction(transaction)) { + throw new DuplicateTransactionException(); + } + transactionList.add(transaction); + } + + /** + * Removes {@code key} from this {@code TransactionBook}. {@code key} must exist in the transaction book. + * + * @return whether the removal was successful + */ + public boolean removeTransaction(Transaction key) { + requireNonNull(key); + return transactionList.remove(key); + } + + /** + * Removes all txns involving person with {@code personId}. + */ + public void deleteTransactionsOfPersonId(String personId) { + requireNonNull(personId); + List txnList = transactionList.stream().filter((t) -> !t.isByPersonId(personId)).toList(); + transactionList.clear(); + transactionList.addAll(txnList); + } + + @Override + public ObservableList getTransactionList() { + return transactionUnmodifiableList; + } + + @Override + public String toString() { + return transactionList.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof TransactionBook)) { + return false; + } + + TransactionBook otherTransactionBook = (TransactionBook) other; + return transactionList.equals(otherTransactionBook.transactionList); + } + + @Override + public int hashCode() { + return transactionList.hashCode(); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/TransactionBookModel.java b/src/main/java/spleetwaise/transaction/model/TransactionBookModel.java new file mode 100644 index 00000000000..97c89da37cf --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/TransactionBookModel.java @@ -0,0 +1,72 @@ +package spleetwaise.transaction.model; + +import java.util.function.Predicate; + +import javafx.beans.property.ObjectProperty; +import javafx.collections.ObservableList; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * The API of the transaction component. + */ +public interface TransactionBookModel { + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_TXNS = unused -> true; + + /** + * Returns the transaction book. + */ + ReadOnlyTransactionBook getTransactionBook(); + + /** + * Replaces address book data with the data in {@code replacementBook}. + */ + void setTransactionBook(ReadOnlyTransactionBook replacementBook); + + /** + * Adds the given transaction. {@code transaction} must not already exist in the address book. + */ + void addTransaction(Transaction transaction); + + /** + * Returns true if a transaction with the same details as an existing transaction exist in the transaction book. + */ + boolean hasTransaction(Transaction transaction); + + /** + * Returns an unmodifiable view of the filtered transaction list. + */ + ObservableList getFilteredTransactionList(); + + /** + * Returns the current predicate used to filter transactions. + */ + ObjectProperty> getCurrentPredicate(); + + /** + * Updates the filter of the filtered transaction list to reuse existing filter. By default, existing filter is show + * all transactions. + */ + void updateFilteredTransactionList(); + + /** + * Updates the filter of the filtered transaction list to filter by the given {@code predicate}. Set to null to + * clear existing filters. + */ + void updateFilteredTransactionList(Predicate predicate); + + /** + * Deletes transactions of a person with the given id. Used for when a person is deleted. + */ + void deleteTransactionsOfPersonId(String personId); + + /** + * Deletes the given transaction. Transaction must be present in the transactionBook. + */ + void deleteTransaction(Transaction target); + + /** + * Replaces the given transaction. Transaction must be present in the transactionBook. + */ + void setTransaction(Transaction target, Transaction editedTransaction); +} diff --git a/src/main/java/spleetwaise/transaction/model/TransactionBookModelManager.java b/src/main/java/spleetwaise/transaction/model/TransactionBookModelManager.java new file mode 100644 index 00000000000..c89a4b7272c --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/TransactionBookModelManager.java @@ -0,0 +1,136 @@ +package spleetwaise.transaction.model; + +import static java.util.Objects.requireNonNull; +import static spleetwaise.commons.util.CollectionUtil.requireAllNonNull; + +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 spleetwaise.commons.core.LogsCenter; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Represents the in-memory model of the transaction data. + */ +public class TransactionBookModelManager implements TransactionBookModel { + + private static final Logger logger = LogsCenter.getLogger(TransactionBookModelManager.class); + private ObjectProperty> currPredicate = new SimpleObjectProperty<>( + TransactionBookModel.PREDICATE_SHOW_ALL_TXNS); + + private final TransactionBook transactionBook; + private final FilteredList filteredTransactions; + + /** + * Initializes a TransactionBookModelManager with the given transactionBook. + */ + public TransactionBookModelManager(TransactionBook transactionBook) { + requireNonNull(transactionBook); + + logger.fine("Initializing Transaction Model..."); + + this.transactionBook = new TransactionBook(transactionBook); + filteredTransactions = new FilteredList<>(this.transactionBook.getTransactionList()); + } + + /** + * Initializes a TransactionBookModelManager with the readonly transactionBook. + */ + public TransactionBookModelManager(ReadOnlyTransactionBook transactionBook) { + requireNonNull(transactionBook); + + logger.fine("Initializing Transaction Model..."); + + this.transactionBook = new TransactionBook(transactionBook); + filteredTransactions = new FilteredList<>(this.transactionBook.getTransactionList()); + } + + public TransactionBookModelManager() { + this(new TransactionBook()); + } + + @Override + public ReadOnlyTransactionBook getTransactionBook() { + return transactionBook; + } + + @Override + public void setTransactionBook(ReadOnlyTransactionBook replacementBook) { + requireNonNull(replacementBook); + transactionBook.setTransactionBook(replacementBook); + } + + @Override + public void addTransaction(Transaction transaction) { + transactionBook.addTransaction(transaction); + updateFilteredTransactionList(); + } + + @Override + public boolean hasTransaction(Transaction transaction) { + requireNonNull(transaction); + return transactionBook.containsTransaction(transaction); + } + + @Override + public ObservableList getFilteredTransactionList() { + return filteredTransactions; + } + + @Override + public void updateFilteredTransactionList() { + filteredTransactions.setPredicate(currPredicate.get()); + } + + @Override + public void updateFilteredTransactionList(Predicate predicate) { + currPredicate.set(predicate); + filteredTransactions.setPredicate(predicate); + } + + @Override + public ObjectProperty> getCurrentPredicate() { + return currPredicate; + } + + @Override + public void deleteTransactionsOfPersonId(String personId) { + requireNonNull(personId); + transactionBook.deleteTransactionsOfPersonId(personId); + } + + @Override + public void deleteTransaction(Transaction transaction) { + requireNonNull(transaction); + transactionBook.removeTransaction(transaction); + updateFilteredTransactionList(); + } + + @Override + public void setTransaction(Transaction target, Transaction editedTransaction) { + requireAllNonNull(target, editedTransaction); + + transactionBook.setTransaction(target, editedTransaction); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof TransactionBookModel)) { + return false; + } + + TransactionBookModelManager otherModelManager = (TransactionBookModelManager) other; + return transactionBook.equals(otherModelManager.transactionBook) + && filteredTransactions.equals(otherModelManager.filteredTransactions); + } + +} diff --git a/src/main/java/spleetwaise/transaction/model/filterpredicate/AmountFilterPredicate.java b/src/main/java/spleetwaise/transaction/model/filterpredicate/AmountFilterPredicate.java new file mode 100644 index 00000000000..bac3612ec0d --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/filterpredicate/AmountFilterPredicate.java @@ -0,0 +1,53 @@ +package spleetwaise.transaction.model.filterpredicate; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Amount; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Tests if a {@code Transaction}'s {@code Amount} matches the given {@code Amount}. + */ +public class AmountFilterPredicate implements Predicate { + + private final Amount amountToFilter; + + /** + * Creates a {@code AmountFilterPredicate} that tests if a {@code Transaction}'s {@code Amount} matches the + * given {@code Amount}. + * + * @param amountToFilter The {@code Amount} to filter by. + */ + public AmountFilterPredicate(Amount amountToFilter) { + requireNonNull(amountToFilter); + this.amountToFilter = amountToFilter; + } + + @Override + public boolean test(Transaction transaction) { + return transaction.getAmount().equals(amountToFilter); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AmountFilterPredicate otherAmountFilterPredicate)) { + return false; + } + + return amountToFilter.equals(otherAmountFilterPredicate.amountToFilter); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("amount", amountToFilter) + .toString(); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/filterpredicate/AmountSignFilterPredicate.java b/src/main/java/spleetwaise/transaction/model/filterpredicate/AmountSignFilterPredicate.java new file mode 100644 index 00000000000..b401066ddca --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/filterpredicate/AmountSignFilterPredicate.java @@ -0,0 +1,70 @@ +package spleetwaise.transaction.model.filterpredicate; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +import spleetwaise.commons.util.AppUtil; +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Tests if a {@code Transaction}'s {@code Amount} has a positive or negative sign. + */ +public class AmountSignFilterPredicate implements Predicate { + + public static final String POSITIVE_SIGN = "Pos"; + public static final String NEGATIVE_SIGN = "Neg"; + + public static final String MESSAGE_CONSTRAINTS = "Amount sign should be '" + POSITIVE_SIGN + "' or '" + + NEGATIVE_SIGN + "'"; + + private final boolean sign; + + /** + * Creates a {@code AmountSignFilterPredicate} that tests if a {@code Transaction}'s {@code Amount} has a + * positive or negative sign depending on the signString. + * + * @param signString The sign of the {@code Amount} to filter by. + */ + public AmountSignFilterPredicate(String signString) { + requireNonNull(signString); + signString = signString.trim(); + AppUtil.checkArgument(isValidSign(signString), MESSAGE_CONSTRAINTS); + sign = POSITIVE_SIGN.equals(signString); + } + + /** + * Returns true if a given string is a valid amount sign. + */ + public static boolean isValidSign(String sign) { + sign = sign.trim(); + return POSITIVE_SIGN.equals(sign) || NEGATIVE_SIGN.equals(sign); + } + + @Override + public boolean test(Transaction transaction) { + return transaction.getAmount().isNegative() != sign; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AmountSignFilterPredicate otherAmountSignFilterPredicate)) { + return false; + } + + return sign == otherAmountSignFilterPredicate.sign; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("sign", sign) + .toString(); + } + +} diff --git a/src/main/java/spleetwaise/transaction/model/filterpredicate/CategoryFilterPredicate.java b/src/main/java/spleetwaise/transaction/model/filterpredicate/CategoryFilterPredicate.java new file mode 100644 index 00000000000..b83db6ddccf --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/filterpredicate/CategoryFilterPredicate.java @@ -0,0 +1,59 @@ +package spleetwaise.transaction.model.filterpredicate; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Category; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Tests if a {@code Transaction}'s {@code Category} contains the given {@code Category}. + */ +public class CategoryFilterPredicate implements Predicate { + + private final Category categoryToFilter; + + /** + * Creates a {@code CategoryFilterPredicate} that tests if a {@code Transaction}'s {@code Category} contains + * the given {@code Category}. + * + * @param categoryToFilter The {@code Category} to filter by. + */ + public CategoryFilterPredicate(Category categoryToFilter) { + requireNonNull(categoryToFilter); + this.categoryToFilter = categoryToFilter; + } + + /** + * Tests if a {@code Transaction}'s {@code Category} contains the given {@code Category}. + * + * @param transaction The {@code Transaction} to test. + * @return {@code true} if the {@code Transaction}'s {@code Category} contains the given {@code Category}. + */ + @Override + public boolean test(Transaction transaction) { + return transaction.containsCategory(categoryToFilter); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof CategoryFilterPredicate otherCategoryFilterPredicate)) { + return false; + } + + return categoryToFilter.equals(otherCategoryFilterPredicate.categoryToFilter); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("category", categoryToFilter) + .toString(); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/filterpredicate/DateFilterPredicate.java b/src/main/java/spleetwaise/transaction/model/filterpredicate/DateFilterPredicate.java new file mode 100644 index 00000000000..bfa568880b6 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/filterpredicate/DateFilterPredicate.java @@ -0,0 +1,53 @@ +package spleetwaise.transaction.model.filterpredicate; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Date; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Tests if a {@code Transaction}'s {@code Date} matches the given {@code Date}. + */ +public class DateFilterPredicate implements Predicate { + + private final Date dateToFilter; + + /** + * Creates a {@code DateFilterPredicate} that tests if a {@code Transaction}'s {@code Date} matches the + * given {@code Date}. + * + * @param dateToFilter The {@code Date} to filter by. + */ + public DateFilterPredicate(Date dateToFilter) { + requireNonNull(dateToFilter); + this.dateToFilter = dateToFilter; + } + + @Override + public boolean test(Transaction transaction) { + return transaction.getDate().equals(dateToFilter); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof DateFilterPredicate otherDateFilterPredicate)) { + return false; + } + + return dateToFilter.equals(otherDateFilterPredicate.dateToFilter); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("date", dateToFilter) + .toString(); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/filterpredicate/DescriptionFilterPredicate.java b/src/main/java/spleetwaise/transaction/model/filterpredicate/DescriptionFilterPredicate.java new file mode 100644 index 00000000000..d1f4a6be99a --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/filterpredicate/DescriptionFilterPredicate.java @@ -0,0 +1,60 @@ +package spleetwaise.transaction.model.filterpredicate; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Description; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Tests if a {@code Transaction}'s {@code Description} contains the given {@code Description}. + */ +public class DescriptionFilterPredicate implements Predicate { + + private final Description descriptionToFilter; + + /** + * Creates a {@code DescriptionFilterPredicate} that tests if a {@code Transaction}'s {@code Description} contains + * the given {@code Description}. + * + * @param descriptionToFilter The {@code Description} to filter by. + */ + public DescriptionFilterPredicate(Description descriptionToFilter) { + requireNonNull(descriptionToFilter); + this.descriptionToFilter = descriptionToFilter; + } + + /** + * Tests if a {@code Transaction}'s {@code Description} contains the given {@code Description}. + * + * @param transaction The {@code Transaction} to test. + * @return {@code true} if the {@code Transaction}'s {@code Description} contains the given {@code Description}. + */ + @Override + public boolean test(Transaction transaction) { + return transaction.getDescription().toString().toLowerCase() + .contains(descriptionToFilter.toString().toLowerCase()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof DescriptionFilterPredicate otherDescriptionFilterPredicate)) { + return false; + } + + return descriptionToFilter.equals(otherDescriptionFilterPredicate.descriptionToFilter); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("description", descriptionToFilter) + .toString(); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/filterpredicate/PersonFilterPredicate.java b/src/main/java/spleetwaise/transaction/model/filterpredicate/PersonFilterPredicate.java new file mode 100644 index 00000000000..e3886da614f --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/filterpredicate/PersonFilterPredicate.java @@ -0,0 +1,53 @@ +package spleetwaise.transaction.model.filterpredicate; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Tests if a {@code Transaction}'s {@code Person} matches the given {@code Person}. + */ +public class PersonFilterPredicate implements Predicate { + + private final Person personToFilter; + + /** + * Creates a {@code PersonFilterPredicate} that tests if a {@code Transaction}'s {@code Person} matches the + * given {@code Person}. + * + * @param personToFilter The {@code Person} to filter by. + */ + public PersonFilterPredicate(Person personToFilter) { + requireNonNull(personToFilter); + this.personToFilter = personToFilter; + } + + @Override + public boolean test(Transaction transaction) { + return transaction.getPerson().equals(personToFilter); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof PersonFilterPredicate otherPersonFilterPredicate)) { + return false; + } + + return personToFilter.equals(otherPersonFilterPredicate.personToFilter); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("person", personToFilter) + .toString(); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/filterpredicate/StatusFilterPredicate.java b/src/main/java/spleetwaise/transaction/model/filterpredicate/StatusFilterPredicate.java new file mode 100644 index 00000000000..d1291d3d9d2 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/filterpredicate/StatusFilterPredicate.java @@ -0,0 +1,53 @@ +package spleetwaise.transaction.model.filterpredicate; + +import static java.util.Objects.requireNonNull; + +import java.util.function.Predicate; + +import spleetwaise.commons.util.ToStringBuilder; +import spleetwaise.transaction.model.transaction.Status; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Tests if a {@code Transaction}'s {@code Status} matches the given {@code Status}. + */ +public class StatusFilterPredicate implements Predicate { + + private final Status statusToFilter; + + /** + * Creates a {@code StatusFilterPredicate} that tests if a {@code Transaction}'s {@code Status} matches the + * given {@code Status}. + * + * @param statusToFilter The {@code Status} to filter by. + */ + public StatusFilterPredicate(Status statusToFilter) { + requireNonNull(statusToFilter); + this.statusToFilter = statusToFilter; + } + + @Override + public boolean test(Transaction transaction) { + return transaction.getStatus().equals(statusToFilter); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof StatusFilterPredicate otherStatusFilterPredicate)) { + return false; + } + + return statusToFilter.equals(otherStatusFilterPredicate.statusToFilter); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("status", statusToFilter) + .toString(); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/transaction/Amount.java b/src/main/java/spleetwaise/transaction/model/transaction/Amount.java new file mode 100644 index 00000000000..4abbd8105a6 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/transaction/Amount.java @@ -0,0 +1,81 @@ +package spleetwaise.transaction.model.transaction; + +import static java.util.Objects.requireNonNull; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import spleetwaise.commons.util.AppUtil; + +/** + * Represents a Transaction's amount in the transaction book. Guarantees: immutable; is valid or declared in + * {@link #isValidAmount(String)} + */ +public class Amount { + + public static final String MESSAGE_CONSTRAINTS = + "Amount should only contain digits up to 2 decimal points delimited by . and prefixed with - for negative" + + " amounts"; + + /* + * The first character of amount must be + or - and only allow precision up to 2 decimal places + */ + public static final String VALIDATION_REGEX = "^(\\-)?([\\d]+$|[\\d]+\\.[\\d]{1,2}$)"; + + private static final int MAX_DECIMAL_PLACES = 2; + + public final BigDecimal amount; + + /** + * Constructs a {@code Amount} + * + * @param amount A valid amount. + */ + public Amount(String amount) { + requireNonNull(amount); + String trimmedAmount = amount.trim(); + AppUtil.checkArgument(isValidAmount(trimmedAmount), MESSAGE_CONSTRAINTS); + this.amount = new BigDecimal(trimmedAmount).setScale(MAX_DECIMAL_PLACES, RoundingMode.HALF_UP); + } + + /** + * Check if test string provided passes validation and is non-zero. + * + * @param test The test string to test. + * @return Returns true if the test string passes the regex check and is non-zero value + */ + public static boolean isValidAmount(String test) { + return test.trim().matches(VALIDATION_REGEX) && new BigDecimal(test.trim()).compareTo(BigDecimal.ZERO) != 0; + } + + public BigDecimal getAmount() { + return amount; + } + + public boolean isNegative() { + return amount.compareTo(BigDecimal.ZERO) < 0; + } + + @Override + public String toString() { + return String.format("%.2f", amount); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Amount otherAmount)) { + return false; + } + + return otherAmount.amount.equals(amount); + } + + @Override + public int hashCode() { + return amount.hashCode(); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/transaction/Category.java b/src/main/java/spleetwaise/transaction/model/transaction/Category.java new file mode 100644 index 00000000000..d8a1c7e0056 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/transaction/Category.java @@ -0,0 +1,61 @@ +package spleetwaise.transaction.model.transaction; + +import static java.util.Objects.requireNonNull; + +import spleetwaise.commons.util.AppUtil; + +/** + * Represents a Transaction's categories in the transaction book. + */ +public class Category { + + public static final String MESSAGE_CONSTRAINTS = "Categories should be alphanumeric"; + public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + + public final String category; + + /** + * Constructs a {@code Category}. + * + * @param category A valid tag name. + */ + public Category(String category) { + requireNonNull(category); + String trimmedCategory = category.trim(); + AppUtil.checkArgument(isValidCatName(trimmedCategory), MESSAGE_CONSTRAINTS); + this.category = trimmedCategory; + } + + /** + * Returns true if a given string is a valid category format. + */ + public static boolean isValidCatName(String test) { + return test.trim().matches(VALIDATION_REGEX); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Category otherCategory)) { + return false; + } + + return category.equals(otherCategory.category); + } + + @Override + public int hashCode() { + return category.hashCode(); + } + + /** + * Format state as text for viewing. + */ + public String toString() { + return category; + } +} diff --git a/src/main/java/spleetwaise/transaction/model/transaction/Date.java b/src/main/java/spleetwaise/transaction/model/transaction/Date.java new file mode 100644 index 00000000000..9a93816b7b3 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/transaction/Date.java @@ -0,0 +1,82 @@ +package spleetwaise.transaction.model.transaction; + +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; + +import spleetwaise.commons.util.AppUtil; + +/** + * Represents a Transaction's date in the transaction book. Guarantees: immutable; is valid or declared in + * {@link #isValidDate(String)} + */ +public class Date { + public static final String MESSAGE_CONSTRAINTS = "Date should be valid and in the format of DDMMYYYY"; + + public static final DateTimeFormatter VALIDATION_FORMATTER = DateTimeFormatter.ofPattern("ddMMuuuu") + .withResolverStyle(ResolverStyle.STRICT); + + public static final DateTimeFormatter DISPLAY_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/uuuu"); + + public final LocalDate date; + + /** + * Constructs a {@code Date} + * + * @param date A valid date string. + */ + public Date(String date) { + requireNonNull(date); + String trimmedDate = date.trim(); + AppUtil.checkArgument(isValidDate(trimmedDate), MESSAGE_CONSTRAINTS); + this.date = LocalDate.parse(trimmedDate, VALIDATION_FORMATTER); + } + + /** + * Returns true if a given string is a valid date. + */ + public static boolean isValidDate(String test) { + try { + LocalDate.parse(test.trim(), VALIDATION_FORMATTER); + return true; + } catch (DateTimeParseException e) { + return false; + } + } + + public static String getNowDate() { + return LocalDate.now().format(VALIDATION_FORMATTER); + } + + public LocalDate getDate() { + return date; + } + + @Override + public String toString() { + return date.format(DISPLAY_FORMATTER); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Date)) { + return false; + } + + Date otherDate = (Date) other; + return this.date.equals(otherDate.date); + } + + @Override + public int hashCode() { + return this.date.hashCode(); + } + +} diff --git a/src/main/java/spleetwaise/transaction/model/transaction/Description.java b/src/main/java/spleetwaise/transaction/model/transaction/Description.java new file mode 100644 index 00000000000..85d4bbd13bd --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/transaction/Description.java @@ -0,0 +1,63 @@ +package spleetwaise.transaction.model.transaction; + +import static java.util.Objects.requireNonNull; + +import spleetwaise.commons.util.AppUtil; + +/** + * Represents a Transaction's description in the transaction book. Guarantees: immutable; is valid as declared in + * {@link #isValidDescription(String)} + */ +public class Description { + + /** + * Maximum length of description + */ + public static final int MAX_LENGTH = 120; + + public static final String MESSAGE_CONSTRAINTS = "Description should not be blank or more than " + MAX_LENGTH + + " characters."; + + public final String description; + + /** + * Constructs a {@code Description} + * + * @param description A valid description. + */ + public Description(String description) { + requireNonNull(description); + description = description.trim(); + AppUtil.checkArgument(isValidDescription(description), MESSAGE_CONSTRAINTS); + this.description = description; + } + + public static boolean isValidDescription(String testString) { + return !testString.trim().isEmpty() && testString.trim().length() <= MAX_LENGTH; + } + + @Override + public String toString() { + return description; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Description)) { + return false; + } + + Description otherDescription = (Description) other; + return description.equals(otherDescription.description); + } + + @Override + public int hashCode() { + return description.hashCode(); + } +} + diff --git a/src/main/java/spleetwaise/transaction/model/transaction/Status.java b/src/main/java/spleetwaise/transaction/model/transaction/Status.java new file mode 100644 index 00000000000..27d756463a5 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/transaction/Status.java @@ -0,0 +1,80 @@ +package spleetwaise.transaction.model.transaction; + +import static java.util.Objects.requireNonNull; + +import spleetwaise.commons.util.AppUtil; + +/** + * Represents the status of a transaction, indicating whether it is done or not. + */ +public class Status { + + public static final String DONE_STATUS = "Done"; + public static final String NOT_DONE_STATUS = "Not Done"; + + public static final String MESSAGE_CONSTRAINTS = "Status should be '" + DONE_STATUS + "' or '" + NOT_DONE_STATUS + + "'"; + + private final boolean isDone; + + /** + * Constructs a {@code Status} with the specified completion state. + * + * @param isDone {@code true} if the status is marked as done, {@code false} otherwise. + */ + public Status(boolean isDone) { + this.isDone = isDone; + } + + /** + * Constructs a {@code Status} with the specified isDoneString. + * + * @param isDoneString The string representation of the completion state. + */ + public Status(String isDoneString) { + requireNonNull(isDoneString); + String isDoneStringTrimmed = isDoneString.trim(); + AppUtil.checkArgument(isValidStatus(isDoneStringTrimmed), MESSAGE_CONSTRAINTS); + this.isDone = DONE_STATUS.equals(isDoneStringTrimmed); + } + + /** + * Returns true if a given string is a valid status. + */ + public static boolean isValidStatus(String status) { + String trimmedStatus = status.trim(); + return DONE_STATUS.equals(trimmedStatus) || NOT_DONE_STATUS.equals(trimmedStatus); + } + + /** + * Returns the completion state of the status. + * + * @return {@code true} if the status is marked as done, {@code false} otherwise. + */ + public boolean isDone() { + return isDone; + } + + @Override + public String toString() { + return isDone ? DONE_STATUS : NOT_DONE_STATUS; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Status otherStatus)) { + return false; + } + + return otherStatus.isDone == isDone; + } + + @Override + public int hashCode() { + return Boolean.hashCode(isDone); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/transaction/Transaction.java b/src/main/java/spleetwaise/transaction/model/transaction/Transaction.java new file mode 100644 index 00000000000..03ad344bd59 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/transaction/Transaction.java @@ -0,0 +1,164 @@ +package spleetwaise.transaction.model.transaction; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.util.CollectionUtil; +import spleetwaise.commons.util.IdUtil; + +/** + * Represents a Transaction in the transaction book. Guarantees: details are present and not null, field values are + * validated, immutable. + */ +public class Transaction { + + private final String id; + private final Person person; + private final Amount amount; + private final Description description; + private final Date date; + private final Set categories = new HashSet<>(); + private final Status status; + + /** + * Represents a Transaction in the transaction book. + * + * @param id The id of this transaction. + * @param person The person involved in this transaction. + * @param amount The amount involved in this transaction. + * @param description The description of the transaction. + * @param date The date the transaction has taken place. + * @param categories The categories the transaction has. + */ + public Transaction( + String id, Person person, Amount amount, Description description, Date date, + Set categories, Status status + ) { + CollectionUtil.requireAllNonNull(person, amount, description, date, categories, status); + this.id = id; + this.person = person; + this.amount = amount; + this.description = description; + this.date = date; + this.categories.addAll(categories); + this.status = status; + } + + /** + * Represents a Transaction in the transaction book. + * + * @param person The person involved in this transaction. + * @param amount The amount involved in this transaction. + * @param description The description of the transaction. + * @param date The date the transaction has taken place. + * @param categories The categories the transaction has. + */ + public Transaction( + Person person, Amount amount, Description description, Date date, Set categories, + Status status + ) { + this(IdUtil.getId(), person, amount, description, date, categories, status); + } + + public String getId() { + return id; + } + + public Person getPerson() { + return person; + } + + public Amount getAmount() { + return amount; + } + + public Description getDescription() { + return description; + } + + public Date getDate() { + return date; + } + + public Status getStatus() { + return status; + } + + /** + * Returns a new transaction with specified status. + * + * @param status The done status to be set + * @return new transaction with updated status + */ + public Transaction setStatus(Status status) { + return new Transaction(id, person, amount, description, date, categories, status); + } + + /** + * Returns a boolean value if the transaction contains the category + */ + public boolean containsCategory(Category category) { + return categories.contains(category); + } + + /** + * Returns an immutable category set, which throws {@code UnsupportedOperationException} if modification is + * attempted. + */ + public Set getCategories() { + return Collections.unmodifiableSet(categories); + } + + /** + * Returns true if both transactions have the same uuid. + * + * @param other The other object to compare + * @return true of the object + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Transaction otherTransaction)) { + return false; + } + return this.person.equals(otherTransaction.getPerson()) + && this.amount.equals(otherTransaction.getAmount()) + && this.description.equals(otherTransaction.getDescription()) + && this.date.equals(otherTransaction.getDate()); + } + + /** + * Returns true if both txns have the same id. + */ + public boolean hasSameId(Transaction otherTxn) { + requireNonNull(otherTxn); + return otherTxn.getId().equals(getId()); + } + + /** + * Returns true if txn has person with personId. + */ + public boolean isByPersonId(String personId) { + requireNonNull(personId); + return this.person.getId().equals(personId); + } + + @Override + public int hashCode() { + return this.id.hashCode(); + } + + @Override + public String toString() { + return String.format("%s [%s] (%s): %s on %s for $%s with categories: %s", person.getName(), status, + person.getPhone(), description, date, amount, categories + ); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/transaction/TransactionIdPredicate.java b/src/main/java/spleetwaise/transaction/model/transaction/TransactionIdPredicate.java new file mode 100644 index 00000000000..b7279e28fd2 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/transaction/TransactionIdPredicate.java @@ -0,0 +1,20 @@ +package spleetwaise.transaction.model.transaction; + +import java.util.function.Predicate; + +/** + * Tests that a {@code Transaction}'s {@code Id} matches a transactionId given. + */ +public class TransactionIdPredicate implements Predicate { + private final String transactionId; + + public TransactionIdPredicate(String transactionId) { + this.transactionId = transactionId; + } + + @Override + public boolean test(Transaction transaction) { + return transaction.getId().equals(transactionId); + } +} + diff --git a/src/main/java/spleetwaise/transaction/model/transaction/exceptions/DuplicateTransactionException.java b/src/main/java/spleetwaise/transaction/model/transaction/exceptions/DuplicateTransactionException.java new file mode 100644 index 00000000000..507ea79e76b --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/transaction/exceptions/DuplicateTransactionException.java @@ -0,0 +1,10 @@ +package spleetwaise.transaction.model.transaction.exceptions; + +/** + * Catches errors that are causes duplicates in the transaction book. + */ +public class DuplicateTransactionException extends RuntimeException { + public DuplicateTransactionException() { + super("Operation would result in duplicate transactions"); + } +} diff --git a/src/main/java/spleetwaise/transaction/model/transaction/exceptions/TransactionNotFoundException.java b/src/main/java/spleetwaise/transaction/model/transaction/exceptions/TransactionNotFoundException.java new file mode 100644 index 00000000000..3e4819a452f --- /dev/null +++ b/src/main/java/spleetwaise/transaction/model/transaction/exceptions/TransactionNotFoundException.java @@ -0,0 +1,7 @@ +package spleetwaise.transaction.model.transaction.exceptions; + +/** + * Signals that the operation is unable to find the specified transaction. + */ +public class TransactionNotFoundException extends RuntimeException { +} diff --git a/src/main/java/spleetwaise/transaction/storage/JsonSerializableTransactionBook.java b/src/main/java/spleetwaise/transaction/storage/JsonSerializableTransactionBook.java new file mode 100644 index 00000000000..9c779606990 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/storage/JsonSerializableTransactionBook.java @@ -0,0 +1,75 @@ +package spleetwaise.transaction.storage; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import spleetwaise.address.model.AddressBookModel; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.exceptions.IllegalValueException; +import spleetwaise.transaction.model.ReadOnlyTransactionBook; +import spleetwaise.transaction.model.TransactionBook; +import spleetwaise.transaction.model.transaction.Transaction; +import spleetwaise.transaction.storage.adapters.JsonAdaptedTransaction; + +/** + * An Immutable AddressBook that is serializable to JSON format. + */ +@JsonRootName(value = "transactionbook") +class JsonSerializableTransactionBook { + + public static final String MESSAGE_DUPLICATE_TRANSACTIONS = "Transaction list contains duplicate transaction(s)."; + private static final Logger logger = LogsCenter.getLogger(JsonSerializableTransactionBook.class); + private final List transactions = new ArrayList<>(); + + /** + * Constructs a {@code JsonSerializableAddressBook} with the given persons. + */ + @JsonCreator + public JsonSerializableTransactionBook(@JsonProperty("transactions") List txns) { + this.transactions.addAll(txns); + } + + /** + * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableAddressBook}. + */ + public JsonSerializableTransactionBook(ReadOnlyTransactionBook source) { + transactions.addAll( + source.getTransactionList().stream().map(JsonAdaptedTransaction::new).collect(Collectors.toList())); + } + + /** + * Converts this address book into the model's {@code AddressBook} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public TransactionBook toModelType(AddressBookModel addressBookModel) throws IllegalValueException { + requireNonNull(addressBookModel); + TransactionBook transactionBook = new TransactionBook(); + for (JsonAdaptedTransaction jsonAdaptedTxn : transactions) { + try { + Transaction txn = jsonAdaptedTxn.toModelType(addressBookModel); + if (transactionBook.containsTransaction(txn) || transactionBook.containsTransactionById(txn)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_TRANSACTIONS); + } + transactionBook.addTransaction(txn); + } catch (IllegalValueException e) { + logger.warning(String.format( + "Transaction book is possibly corrupted: %s Ignoring corrupted transactions.", + e.getMessage() + )); + } + } + return transactionBook; + } + +} diff --git a/src/main/java/spleetwaise/transaction/storage/JsonTransactionBookStorage.java b/src/main/java/spleetwaise/transaction/storage/JsonTransactionBookStorage.java new file mode 100644 index 00000000000..db0da60a2a7 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/storage/JsonTransactionBookStorage.java @@ -0,0 +1,87 @@ +package spleetwaise.transaction.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import spleetwaise.address.model.AddressBookModel; +import spleetwaise.address.storage.JsonAddressBookStorage; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.exceptions.DataLoadingException; +import spleetwaise.commons.exceptions.IllegalValueException; +import spleetwaise.commons.util.FileUtil; +import spleetwaise.commons.util.JsonUtil; +import spleetwaise.transaction.model.ReadOnlyTransactionBook; + + +/** + * A class to access TransactionBook data stored as a json file on the hard disk. + */ +public class JsonTransactionBookStorage implements TransactionBookStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonAddressBookStorage.class); + + private Path filePath; + + public JsonTransactionBookStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getTransactionBookFilePath() { + return filePath; + } + + @Override + public Optional readTransactionBook(AddressBookModel addressBookModel) + throws DataLoadingException { + return readTransactionBook(filePath, addressBookModel); + } + + /** + * Reads transaction book from file + * + * @param filePath location of the data. Cannot be null. + * @param addressBookModel reference to address book. Cannot be null. + * @throws DataLoadingException if loading the data from storage failed. + */ + public Optional readTransactionBook(Path filePath, AddressBookModel addressBookModel) + throws DataLoadingException { + requireNonNull(filePath); + requireNonNull(addressBookModel); + + Optional jsonTransactionBook = JsonUtil.readJsonFile( + filePath, JsonSerializableTransactionBook.class); + if (!jsonTransactionBook.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonTransactionBook.get().toModelType(addressBookModel)); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataLoadingException(ive); + } + } + + @Override + public void saveTransactionBook(ReadOnlyTransactionBook txnBook) throws IOException { + saveTransactionBook(txnBook, filePath); + } + + /** + * Similar to {@link #saveTransactionBook(ReadOnlyTransactionBook)}. + * + * @param filePath location of the data. Cannot be null. + */ + public void saveTransactionBook(ReadOnlyTransactionBook txnBook, Path filePath) throws IOException { + requireNonNull(txnBook); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableTransactionBook(txnBook), filePath); + } + +} diff --git a/src/main/java/spleetwaise/transaction/storage/TransactionBookStorage.java b/src/main/java/spleetwaise/transaction/storage/TransactionBookStorage.java new file mode 100644 index 00000000000..a50f3259ea3 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/storage/TransactionBookStorage.java @@ -0,0 +1,49 @@ +package spleetwaise.transaction.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import spleetwaise.address.model.AddressBookModel; +import spleetwaise.commons.exceptions.DataLoadingException; +import spleetwaise.transaction.model.ReadOnlyTransactionBook; +import spleetwaise.transaction.model.TransactionBook; + +/** + * Represents a storage for {@link TransactionBook}. + */ +public interface TransactionBookStorage { + + /** + * Returns the file path of the data file. + */ + Path getTransactionBookFilePath(); + + /** + * Returns TransactionBook data as a {@link ReadOnlyTransactionBook}. Returns {@code Optional.empty()} if storage + * file is not found. + * + * @throws DataLoadingException if loading the data from storage failed. + */ + Optional readTransactionBook(AddressBookModel addressBookModel) + throws DataLoadingException; + + /** + * @see #getTransactionBookFilePath() + */ + Optional readTransactionBook(Path filePath, AddressBookModel addressBookModel) + throws DataLoadingException; + + /** + * Saves the given {@link ReadOnlyTransactionBook} to the storage. + * + * @param transactionBook cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveTransactionBook(ReadOnlyTransactionBook transactionBook) throws IOException; + + /** + * @see #saveTransactionBook(ReadOnlyTransactionBook) + */ + void saveTransactionBook(ReadOnlyTransactionBook transactionBook, Path filePath) throws IOException; +} diff --git a/src/main/java/spleetwaise/transaction/storage/adapters/JsonAdaptedAmount.java b/src/main/java/spleetwaise/transaction/storage/adapters/JsonAdaptedAmount.java new file mode 100644 index 00000000000..50c37a063e9 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/storage/adapters/JsonAdaptedAmount.java @@ -0,0 +1,64 @@ +package spleetwaise.transaction.storage.adapters; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import spleetwaise.commons.exceptions.IllegalValueException; +import spleetwaise.transaction.model.transaction.Amount; + +/** + * This package contains adapters for converting between Amount objects in our model and other formats. + */ +public class JsonAdaptedAmount { + + /** + * The amount to be stored or retrieved. + * + * @param amount the amount as a string + */ + private final String amount; + + /** + * Create a new instance of JsonAdaptedAmount from a string. This will throw an exception if the string is not a + * valid amount. + * + * @param amount the amount as a string + * @throws IllegalValueException if the string is not a valid amount + */ + @JsonCreator + public JsonAdaptedAmount(@JsonProperty("amount") String amount) { + this.amount = amount; + } + + /** + * Create a new instance of JsonAdaptedAmount from an Amount object. + * + * @param amount the Amount to convert from + */ + public JsonAdaptedAmount(Amount amount) { + this.amount = amount.toString(); + } + + /** + * Convert this JsonAdaptedAmount back into a model type Amount. This will throw an exception if the string is not a + * valid amount. + * + * @return the Amount + * @throws IllegalValueException if the string is not a valid amount + */ + public Amount toModelType() throws IllegalValueException { + if (!Amount.isValidAmount(amount)) { + throw new IllegalValueException(Amount.MESSAGE_CONSTRAINTS); + } + return new Amount(amount); + } + + /** + * Retrieves the amount as a string. + * + * @return The amount + */ + public String getAmount() { + return amount; + } +} diff --git a/src/main/java/spleetwaise/transaction/storage/adapters/JsonAdaptedCategory.java b/src/main/java/spleetwaise/transaction/storage/adapters/JsonAdaptedCategory.java new file mode 100644 index 00000000000..751bc40af3d --- /dev/null +++ b/src/main/java/spleetwaise/transaction/storage/adapters/JsonAdaptedCategory.java @@ -0,0 +1,47 @@ +package spleetwaise.transaction.storage.adapters; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import spleetwaise.commons.exceptions.IllegalValueException; +import spleetwaise.transaction.model.transaction.Category; + +/** + * Jackson-friendly version of {@link Category}. + */ +public class JsonAdaptedCategory { + + private final String category; + + /** + * Constructs a {@code JsonAdaptedCategory} with the given {@code category}. + */ + @JsonCreator + public JsonAdaptedCategory(String category) { + this.category = category; + } + + /** + * Converts a given {@code Category} into this class for Jackson use. + */ + public JsonAdaptedCategory(Category source) { + category = source.category; + } + + @JsonValue + public String getCategory() { + return category; + } + + /** + * Converts this Jackson-friendly adapted category object into the model's {@code Category} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted cat. + */ + public Category toModelType() throws IllegalValueException { + if (!Category.isValidCatName(category)) { + throw new IllegalValueException(Category.MESSAGE_CONSTRAINTS); + } + return new Category(category); + } +} diff --git a/src/main/java/spleetwaise/transaction/storage/adapters/JsonAdaptedTransaction.java b/src/main/java/spleetwaise/transaction/storage/adapters/JsonAdaptedTransaction.java new file mode 100644 index 00000000000..9f0573a05fa --- /dev/null +++ b/src/main/java/spleetwaise/transaction/storage/adapters/JsonAdaptedTransaction.java @@ -0,0 +1,166 @@ +package spleetwaise.transaction.storage.adapters; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import spleetwaise.address.model.AddressBookModel; +import spleetwaise.address.model.person.Person; +import spleetwaise.commons.exceptions.IllegalValueException; +import spleetwaise.commons.util.IdUtil; +import spleetwaise.transaction.model.transaction.Amount; +import spleetwaise.transaction.model.transaction.Category; +import spleetwaise.transaction.model.transaction.Date; +import spleetwaise.transaction.model.transaction.Description; +import spleetwaise.transaction.model.transaction.Status; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Adapter for serializing and deserializing {@link Transaction} objects into JSON. + */ +public class JsonAdaptedTransaction { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Transaction's %s field is missing!"; + public static final String PERSON_ID_NOT_FOUND = "Person with id %s not found!"; + + private final String id; + private final String personId; + private final JsonAdaptedAmount amount; + private final String description; + private final String date; + private final boolean isDone; + private final List categories = new ArrayList<>(); + + /** + * Constructs a {@code JsonAdaptedTransaction} with the given transaction details. + */ + @JsonCreator + public JsonAdaptedTransaction( + @JsonProperty("id") String id, + @JsonProperty("personId") String personId, + @JsonProperty("amount") JsonAdaptedAmount amount, + @JsonProperty("description") String description, + @JsonProperty("date") String date, + @JsonProperty("isDone") boolean isDone, + @JsonProperty("categories") List categories + ) { + this.id = id; + this.personId = personId; + this.amount = amount; + this.description = description; + this.date = date; + this.isDone = isDone; + if (categories != null) { + this.categories.addAll(categories); + } + } + + /** + * Converts a given {@code Transaction} into this class for Jackson use. + */ + public JsonAdaptedTransaction(Transaction transaction) { + this.id = transaction.getId(); + this.personId = transaction.getPerson().getId(); + this.amount = new JsonAdaptedAmount(transaction.getAmount()); + this.description = transaction.getDescription().toString(); + this.date = transaction.getDate().getDate().format(Date.VALIDATION_FORMATTER); + this.isDone = transaction.getStatus().isDone(); + this.categories.addAll(transaction.getCategories().stream().map(JsonAdaptedCategory::new) + .toList()); + } + + public String getId() { + return id; + } + + public JsonAdaptedAmount getAmount() { + return amount; + } + + public String getDescription() { + return description; + } + + public String getDate() { + return date; + } + + public boolean getIsDone() { + return isDone; + } + + /** + * Converts this Jackson-friendly adapted transaction object into the model's {@code Transaction} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted transaction. + */ + public Transaction toModelType(AddressBookModel addressBookModel) throws IllegalValueException { + requireNonNull(addressBookModel); + + final Person person; + final Amount amount; + final Description description; + final Date date; + final Status status; + final List transactionCategories = new ArrayList<>(); + + for (JsonAdaptedCategory category : categories) { + transactionCategories.add(category.toModelType()); + } + + // Check missing fields + if (id == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "id")); + } + + if (!IdUtil.isValidId(id)) { + throw new IllegalValueException(IdUtil.MESSAGE_CONSTRAINTS); + } + + if (personId == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "personId")); + } + + if (this.amount == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Amount.class.getSimpleName())); + } + + if (this.description == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, Description.class.getSimpleName())); + } + + if (this.date == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Date.class.getSimpleName())); + } + + if (!Description.isValidDescription(this.description)) { + throw new IllegalValueException(Description.MESSAGE_CONSTRAINTS); + } + + if (!Date.isValidDate(this.date)) { + throw new IllegalValueException(Date.MESSAGE_CONSTRAINTS); + } + + Optional optionalPerson = addressBookModel.getPersonById(personId); + if (optionalPerson.isEmpty()) { + throw new IllegalValueException(String.format(PERSON_ID_NOT_FOUND, personId)); + } + + person = optionalPerson.get(); + amount = this.amount.toModelType(); + description = new Description(this.description); + date = new Date(this.date); + status = new Status(isDone); + Set categories = new HashSet<>(transactionCategories); + + return new Transaction(id.trim(), person, amount, description, date, categories, status); + } +} diff --git a/src/main/java/spleetwaise/transaction/ui/RightPanel.java b/src/main/java/spleetwaise/transaction/ui/RightPanel.java new file mode 100644 index 00000000000..7e1efee9c48 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/ui/RightPanel.java @@ -0,0 +1,243 @@ +package spleetwaise.transaction.ui; + +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_AMOUNT; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DATE; +import static spleetwaise.transaction.logic.parser.CliSyntax.PREFIX_DESCRIPTION; +import static spleetwaise.transaction.model.filterpredicate.AmountSignFilterPredicate.NEGATIVE_SIGN; +import static spleetwaise.transaction.model.filterpredicate.AmountSignFilterPredicate.POSITIVE_SIGN; +import static spleetwaise.transaction.model.transaction.Status.DONE_STATUS; +import static spleetwaise.transaction.model.transaction.Status.NOT_DONE_STATUS; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.function.Predicate; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.StringBinding; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import spleetwaise.commons.model.CommonModelManager; +import spleetwaise.commons.ui.CommandBox; +import spleetwaise.commons.ui.UiPart; +import spleetwaise.transaction.logic.commands.FilterCommand; +import spleetwaise.transaction.model.FilterCommandPredicate; +import spleetwaise.transaction.model.TransactionBookModel; +import spleetwaise.transaction.model.filterpredicate.AmountSignFilterPredicate; +import spleetwaise.transaction.model.filterpredicate.StatusFilterPredicate; +import spleetwaise.transaction.model.transaction.Amount; +import spleetwaise.transaction.model.transaction.Status; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * A UI component that displays information of {@code Transaction} related information. + */ +public class RightPanel extends UiPart { + private static final String FXML = "RightPanel.fxml"; + + private static String amountSignForFilter = POSITIVE_SIGN; + private static String statusForFilter = DONE_STATUS; + + private TransactionListPanel transactionListPanel; + private CommandBox commandBox; + private ContextMenu filterMenu; + + @FXML + private StackPane transactionListPanelPlaceholder; + @FXML + private Label youOweLabel; + @FXML + private Label youAreOwedLabel; + @FXML + private Button filterBtn; + @FXML + private CheckBox trackUndoneBalanceOnlyCheckBox; + + /** + * Creates a {@code RightPanel} with the given {@code ObservableList} to display. + */ + public RightPanel(CommandBox cb) { + super(FXML); + + // Initial balance track undone balance only + trackUndoneBalanceOnlyCheckBox.setSelected(true); + + commandBox = cb; + CommonModelManager commonModel = CommonModelManager.getInstance(); + + ObservableList txns = commonModel.getFilteredTransactionList(); + // Add listener to automatically update balances when the list changes + txns.addListener((ListChangeListener.Change c) -> updateBalances()); + // Add listener to update balance whenever the checkbox changes + trackUndoneBalanceOnlyCheckBox.selectedProperty() + .addListener((observable, oldValue, newValue) -> updateBalances(newValue)); + + // Initial balance update + updateBalances(); + + transactionListPanel = new TransactionListPanel(txns); + transactionListPanelPlaceholder.getChildren().add(transactionListPanel.getRoot()); + + StringBinding iconColorBinding = Bindings.createStringBinding(() -> commonModel.getCurrentPredicate().get() + == TransactionBookModel.PREDICATE_SHOW_ALL_TXNS ? "gray" : "blue", commonModel.getCurrentPredicate()); + + filterBtn.styleProperty().bind(Bindings.concat("-icon-paint: ", iconColorBinding, ";")); + filterBtn.setPickOnBounds(true); + filterMenu = createFilterMenu(); + filterBtn.addEventHandler(MouseEvent.MOUSE_CLICKED, this::showFilterMenu); + } + + /** + * Updates the balance labels based on whether trackUndoneBalanceOnlyCheckBox is checked. + */ + private void updateBalances() { + updateBalances(trackUndoneBalanceOnlyCheckBox.isSelected()); + } + + /** + * Updates the balance labels based on the current transactions. + */ + private void updateBalances(boolean isTrackingUndoneBalanceOnly) { + ObservableList txns = CommonModelManager.getInstance().getFilteredTransactionList(); + + // Create predicates for each balance type (owe and owed) + FilterCommandPredicate youOweFilter = createBalanceFilter(isTrackingUndoneBalanceOnly, NEGATIVE_SIGN); + FilterCommandPredicate youAreOwedFilter = createBalanceFilter(isTrackingUndoneBalanceOnly, POSITIVE_SIGN); + + youOweLabel.setText( + "You Owe $" + calculateBalance(txns, youOweFilter).toString()); + youAreOwedLabel.setText( + "You are Owed $" + calculateBalance(txns, youAreOwedFilter).toString()); + } + + /** + * Creates a filter predicate based on the undone status and amount sign. + * + * @param isTrackingUndoneBalanceOnly Whether to include only "undone" balances. + * @param sign The sign of the balance (positive for "owed" or negative for "owe"). + * @return A FilterCommandPredicate to apply the specified filters. + */ + private FilterCommandPredicate createBalanceFilter(boolean isTrackingUndoneBalanceOnly, String sign) { + ArrayList> predicates = new ArrayList<>(); + + // Add status predicate if tracking only undone balances + if (isTrackingUndoneBalanceOnly) { + predicates.add(new StatusFilterPredicate(new Status(NOT_DONE_STATUS))); + } + + // Add amount sign predicate + predicates.add(new AmountSignFilterPredicate(sign)); + + return new FilterCommandPredicate(predicates); + } + + /** + * General method to calculate balance based on a filtering condition. + * + * @param txns The list of transactions to filter and sum. + * @param filter The combined predicate to filter transaction. + * @return The sum of amounts that match the filter condition. + */ + private BigDecimal calculateBalance(ObservableList txns, FilterCommandPredicate filter) { + return txns.stream() + .filter(filter) + .map(Transaction::getAmount) + .map(Amount::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + /** + * Displays the filter menu if it is not already showing. + */ + private void showFilterMenu(MouseEvent event) { + if (filterMenu.isShowing()) { + filterMenu.hide(); + } else { + filterMenu.show(filterBtn, event.getScreenX(), event.getScreenY()); + } + } + + /** + * Creates and configures the filter context menu. + * + * @return the configured ContextMenu + */ + private ContextMenu createFilterMenu() { + ContextMenu filterMenu = new ContextMenu(); + + MenuItem resetFilter = new MenuItem("Reset filter"); + resetFilter.setOnAction(e -> resetFilter()); + + MenuItem allTxnByDoneStatus = new MenuItem("Show All Done or Not Done Transactions"); + allTxnByDoneStatus.setOnAction(e -> showAllDoneOrNotDoneTransactions()); + + MenuItem allTxnByAmountSign = new MenuItem("Show All Positive or Negative Transactions"); + allTxnByAmountSign.setOnAction(e -> showAllPositiveOrNegativeTransactions()); + + SeparatorMenuItem divider = new SeparatorMenuItem(); + + MenuItem filterByContact = new MenuItem("Filter by Contact"); + filterByContact.setOnAction( + e -> commandBox.handleFilterCommandEntered(FilterCommand.COMMAND_WORD + " ")); + + MenuItem filterByAmount = new MenuItem("Filter by Amount"); + filterByAmount.setOnAction( + e -> commandBox.handleFilterCommandEntered(FilterCommand.COMMAND_WORD + " " + PREFIX_AMOUNT)); + + MenuItem filterByDescription = new MenuItem("Filter by Description"); + filterByDescription.setOnAction( + e -> commandBox.handleFilterCommandEntered(FilterCommand.COMMAND_WORD + " " + PREFIX_DESCRIPTION)); + + MenuItem filterByDate = new MenuItem("Filter by Date"); + filterByDate.setOnAction( + e -> commandBox.handleFilterCommandEntered(FilterCommand.COMMAND_WORD + " " + PREFIX_DATE)); + + filterMenu.getItems().addAll(resetFilter, allTxnByDoneStatus, allTxnByAmountSign, divider, + filterByContact, filterByAmount, filterByDescription, filterByDate + ); + + return filterMenu; + } + + private void showAllPositiveOrNegativeTransactions() { + // toggle between positive or negative amounts only + applyFilterOnAllTxn(new AmountSignFilterPredicate(amountSignForFilter)); + amountSignForFilter = amountSignForFilter.equals(POSITIVE_SIGN) ? NEGATIVE_SIGN : POSITIVE_SIGN; + } + + private void showAllDoneOrNotDoneTransactions() { + // toggle between done or undone transactions only + applyFilterOnAllTxn(new StatusFilterPredicate(new Status(statusForFilter))); + statusForFilter = statusForFilter.equals(DONE_STATUS) ? NOT_DONE_STATUS : DONE_STATUS; + } + + /** + * Resets the filter to show all transactions, and reset the toggle info. public for testability. + */ + public void resetFilter() { + statusForFilter = DONE_STATUS; + amountSignForFilter = POSITIVE_SIGN; + CommonModelManager.getInstance().updateFilteredTransactionList(TransactionBookModel.PREDICATE_SHOW_ALL_TXNS); + } + + /** + * Apply a filter on all transactions. + * + * @param filter the filter to apply + */ + private void applyFilterOnAllTxn(Predicate filter) { + ArrayList> predicates = new ArrayList<>(); + predicates.add(TransactionBookModel.PREDICATE_SHOW_ALL_TXNS); + predicates.add(filter); + CommonModelManager.getInstance().updateFilteredTransactionList(new FilterCommandPredicate(predicates)); + } +} diff --git a/src/main/java/spleetwaise/transaction/ui/TransactionCard.java b/src/main/java/spleetwaise/transaction/ui/TransactionCard.java new file mode 100644 index 00000000000..853c2ece43b --- /dev/null +++ b/src/main/java/spleetwaise/transaction/ui/TransactionCard.java @@ -0,0 +1,79 @@ +package spleetwaise.transaction.ui; + +import java.time.format.DateTimeFormatter; +import java.util.Comparator; +import java.util.Objects; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.Region; +import spleetwaise.commons.ui.UiPart; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * A UI component that displays information of a {@code Transaction}. + */ +public class TransactionCard extends UiPart { + private static final String FXML = "TransactionListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. As a consequence, UI + * elements' variable names cannot be set to such keywords or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Transaction transaction; + + @FXML + private Label name; + @FXML + private ImageView done; + @FXML + private Label status; + @FXML + private Label amount; + @FXML + private Label description; + @FXML + private Label year; + @FXML + private Label dayMonth; + @FXML + private FlowPane categories; + + /** + * Creates a {@code TransactionCard} with the given {@code Transaction} and index to display. + */ + public TransactionCard(Transaction transaction, int displayedIndex) { + super(FXML); + this.transaction = transaction; + dayMonth.setText(transaction.getDate().getDate().format(DateTimeFormatter.ofPattern("d MMM"))); + year.setText(transaction.getDate().getDate().format(DateTimeFormatter.ofPattern("uuuu"))); + name.setText(displayedIndex + ". " + transaction.getPerson().getName().fullName); + if (transaction.getStatus().isDone()) { + Image doneIcon = new Image( + Objects.requireNonNull(getClass().getResource("/images/done_icon.png")).toExternalForm(), + 25, 0, true, true + ); + done.setImage(doneIcon); + } + description.setText(transaction.getDescription().toString()); + if (transaction.getAmount().isNegative()) { + status.setText("you owe"); + status.setStyle("-fx-text-fill: red;"); + amount.setStyle("-fx-text-fill: red;"); + } else { + status.setText("owes you"); + status.setStyle("-fx-text-fill: green;"); + amount.setStyle("-fx-text-fill: green;"); + } + amount.setText("$" + transaction.getAmount().toString()); + transaction.getCategories().stream() + .sorted(Comparator.comparing(category -> category.category)) + .forEach(category -> categories.getChildren().add(new Label(category.category))); + } +} diff --git a/src/main/java/spleetwaise/transaction/ui/TransactionListPanel.java b/src/main/java/spleetwaise/transaction/ui/TransactionListPanel.java new file mode 100644 index 00000000000..e74fd87aad7 --- /dev/null +++ b/src/main/java/spleetwaise/transaction/ui/TransactionListPanel.java @@ -0,0 +1,48 @@ +package spleetwaise.transaction.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import spleetwaise.commons.core.LogsCenter; +import spleetwaise.commons.ui.UiPart; +import spleetwaise.transaction.model.transaction.Transaction; + +/** + * Panel containing the list of transactions. + */ +public class TransactionListPanel extends UiPart { + private static final String FXML = "TransactionListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(TransactionListPanel.class); + + @javafx.fxml.FXML + private ListView transactionListView; + + /** + * Creates a {@code TransactionListPanel} with the given {@code ObservableList}. + */ + public TransactionListPanel(ObservableList transactionList) { + super(FXML); + transactionListView.setItems(transactionList); + transactionListView.setCellFactory(listView -> new TransactionListPanel.TransactionListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Transaction} using a {@code TransactionCard}. + */ + class TransactionListViewCell extends ListCell { + @Override + protected void updateItem(Transaction transaction, boolean empty) { + super.updateItem(transaction, empty); + + if (empty || transaction == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new TransactionCard(transaction, getIndex() + 1).getRoot()); + } + } + } +} diff --git a/src/main/resources/images/done_icon.png b/src/main/resources/images/done_icon.png new file mode 100644 index 00000000000..bd6392b9a62 Binary files /dev/null and b/src/main/resources/images/done_icon.png differ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 124283a392e..3d7a93d0998 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -2,8 +2,7 @@ - - - + + diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index bfe82a85964..beed4f2db30 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -5,16 +5,16 @@ .list-cell:empty { /* Empty cells will not have alternating colours */ - -fx-background: #383838; + -fx-background: #FEFFFE; } .tag-selector { -fx-border-width: 1; - -fx-border-color: white; + -fx-border-color: #E0E0E0; -fx-border-radius: 3; -fx-background-radius: 3; } .tooltip-text { - -fx-text-fill: white; + -fx-text-fill: #333333; } diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css index 17e8a8722cd..cff4dffa77a 100644 --- a/src/main/resources/view/HelpWindow.css +++ b/src/main/resources/view/HelpWindow.css @@ -1,13 +1,13 @@ #copyButton, #helpMessage { - -fx-text-fill: white; + -fx-text-fill: #333333; } #copyButton { - -fx-background-color: dimgray; + -fx-background-color: #47DDA7; } #copyButton:hover { - -fx-background-color: gray; + -fx-background-color: #3cbc8e; } #copyButton:armed { @@ -15,5 +15,5 @@ } #helpMessageContainer { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#FFFFFF, 20%); } diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index e01f330de33..f237e789fc9 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -2,43 +2,43 @@ - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/LightTheme.css similarity index 52% rename from src/main/resources/view/DarkTheme.css rename to src/main/resources/view/LightTheme.css index 36e6b001cd8..06be4256f0e 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/LightTheme.css @@ -1,26 +1,26 @@ .background { - -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ + -fx-background-color: #FEFFFE; + background-color: #F2F2F2; /* Used in the default.html file */ } .label { -fx-font-size: 11pt; -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: #555555; + -fx-text-fill: #333333; -fx-opacity: 0.9; } .label-bright { -fx-font-size: 11pt; -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: white; + -fx-text-fill: #333333; -fx-opacity: 1; } .label-header { -fx-font-size: 32pt; -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: #333333; -fx-opacity: 1; } @@ -31,6 +31,7 @@ .tab-pane { -fx-padding: 0 0 0 1; + -fx-background-color: #FEFFFE; } .tab-pane .tab-header-area { @@ -40,34 +41,30 @@ } .table-view { - -fx-base: #1d1d1d; - -fx-control-inner-background: #1d1d1d; - -fx-background-color: #1d1d1d; - -fx-table-cell-border-color: transparent; - -fx-table-header-border-color: transparent; + -fx-base: #FEFFFE; + -fx-control-inner-background: #FEFFFE; + -fx-background-color: #F7F7F7; + -fx-table-cell-border-color: #E0E0E0; + -fx-table-header-border-color: #E0E0E0; -fx-padding: 5; } .table-view .column-header-background { - -fx-background-color: transparent; + -fx-background-color: #47DDA7; } .table-view .column-header, .table-view .filler { -fx-size: 35; -fx-border-width: 0 0 1 0; - -fx-background-color: transparent; - -fx-border-color: - transparent - transparent - derive(-fx-base, 80%) - transparent; + -fx-background-color: #FEFFFE; + -fx-border-color: transparent transparent derive(-fx-base, 80%) transparent; -fx-border-insets: 0 10 1 0; } .table-view .column-header .label { -fx-font-size: 20pt; -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: #333333; -fx-alignment: center-left; -fx-opacity: 1; } @@ -77,96 +74,96 @@ } .split-pane:horizontal .split-pane-divider { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #FEFFFE; -fx-border-color: transparent transparent transparent #4d4d4d; } .split-pane { -fx-border-radius: 1; -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #FEFFFE; } .list-view { -fx-background-insets: 0; -fx-padding: 0; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #FEFFFE; } .list-cell { -fx-label-padding: 0 0 0 0; - -fx-graphic-text-gap : 0; + -fx-graphic-text-gap: 0; -fx-padding: 0 0 0 0; } .list-cell:filled:even { - -fx-background-color: #3c3e3f; + -fx-background-color: #FAFAFA; } .list-cell:filled:odd { - -fx-background-color: #515658; + -fx-background-color: #F0F0F0; } .list-cell:filled:selected { - -fx-background-color: #424d5f; + -fx-background-color: #D6F5E5; } .list-cell:filled:selected #cardPane { - -fx-border-color: #3e7b91; + -fx-border-color: #47DDA7; -fx-border-width: 1; } .list-cell .label { - -fx-text-fill: white; + -fx-text-fill: #333333; } .cell_big_label { -fx-font-family: "Segoe UI Semibold"; -fx-font-size: 16px; - -fx-text-fill: #010504; + -fx-text-fill: #333333; } .cell_small_label { -fx-font-family: "Segoe UI"; -fx-font-size: 13px; - -fx-text-fill: #010504; + -fx-text-fill: #333333; } .stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #FEFFFE; } .pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); - -fx-border-top-width: 1px; + -fx-background-color: derive(#FEFFFE, 20%); + -fx-border-color: derive(#E0E0E0, 10%); + -fx-border-top-width: 1px; } .status-bar { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#47DDA7, 30%); } .result-display { - -fx-background-color: transparent; + -fx-background-color: #E0E0E0; -fx-font-family: "Segoe UI Light"; -fx-font-size: 13pt; - -fx-text-fill: white; + -fx-text-fill: #333333; } .result-display .label { - -fx-text-fill: black !important; + -fx-text-fill: #333333 !important; } .status-bar .label { -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: #333333; -fx-padding: 4px; -fx-pref-height: 30px; } .status-bar-with-border { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 25%); + -fx-background-color: derive(#E0F7EF, 30%); + -fx-border-color: derive(#B0E2D3, 25%); -fx-border-width: 1px; } @@ -175,36 +172,40 @@ } .grid-pane { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#FEFFFE, 30%); + -fx-border-color: derive(#E0E0E0, 30%); -fx-border-width: 1px; } .grid-pane .stack-pane { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: #FEFFFE; } .context-menu { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: derive(#FEFFFE, 50%); } .context-menu .label { - -fx-text-fill: white; + -fx-text-fill: #333333; } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#47DDA7, 20%); } .menu-bar .label { -fx-font-size: 14pt; -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: #333333; -fx-opacity: 0.9; } +.menu-bar .button:hover { + -fx-background-color: #47DDA7; +} + .menu .left-container { - -fx-background-color: black; + -fx-background-color: #FEFFFE; } /* @@ -214,27 +215,27 @@ */ .button { -fx-padding: 5 22 5 22; - -fx-border-color: #e2e2e2; + -fx-border-color: #47DDA7; -fx-border-width: 2; -fx-background-radius: 0; - -fx-background-color: #1d1d1d; + -fx-background-color: #FEFFFE; -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; -fx-font-size: 11pt; - -fx-text-fill: #d8d8d8; + -fx-text-fill: #333333; -fx-background-insets: 0 0 0 0, 0, 1, 2; } .button:hover { - -fx-background-color: #3a3a3a; + -fx-background-color: #D6F5E5; } .button:pressed, .button:default:hover:pressed { - -fx-background-color: white; - -fx-text-fill: #1d1d1d; + -fx-background-color: #47DDA7; + -fx-text-fill: #FEFFFE; } .button:focused { - -fx-border-color: white, white; + -fx-border-color: #38c190, #38c190; -fx-border-width: 1, 1; -fx-border-style: solid, segments(1, 1); -fx-border-radius: 0, 0; @@ -243,50 +244,50 @@ .button:disabled, .button:default:disabled { -fx-opacity: 0.4; - -fx-background-color: #1d1d1d; - -fx-text-fill: white; + -fx-background-color: #F2F2F2; + -fx-text-fill: #A0A0A0; } .button:default { - -fx-background-color: -fx-focus-color; - -fx-text-fill: #ffffff; + -fx-background-color: #47DDA7; + -fx-text-fill: #FEFFFE; } .button:default:hover { - -fx-background-color: derive(-fx-focus-color, 30%); + -fx-background-color: derive(#47DDA7, 30%); } .dialog-pane { - -fx-background-color: #1d1d1d; + -fx-background-color: #FEFFFE; } .dialog-pane > *.button-bar > *.container { - -fx-background-color: #1d1d1d; + -fx-background-color: #E0E0E0; } .dialog-pane > *.label.content { -fx-font-size: 14px; -fx-font-weight: bold; - -fx-text-fill: white; + -fx-text-fill: #333333; } .dialog-pane:header *.header-panel { - -fx-background-color: derive(#1d1d1d, 25%); + -fx-background-color: derive(#E0E0E0, 25%); } .dialog-pane:header *.header-panel *.label { -fx-font-size: 18px; -fx-font-style: italic; - -fx-fill: white; - -fx-text-fill: white; + -fx-fill: #333333; + -fx-text-fill: #333333; } .scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#E0E0E0, 20%); } .scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: derive(#E0E0E0, 50%); -fx-background-insets: 3; } @@ -318,14 +319,14 @@ } #commandTextField { - -fx-background-color: transparent #383838 transparent #383838; + -fx-background-color: transparent #E0E0E0 transparent #E0E0E0; -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; + -fx-border-color: #FEFFFE #FEFFFE #383838 #FEFFFE; -fx-border-insets: 0; -fx-border-width: 1; -fx-font-family: "Segoe UI Light"; -fx-font-size: 13pt; - -fx-text-fill: white; + -fx-text-fill: #333333; } #filterField, #personListPanel, #personWebpage { @@ -333,20 +334,85 @@ } #resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; + -fx-background-color: transparent, #E0E0E0, transparent, #E0E0E0; -fx-background-radius: 0; } -#tags { +#tags, #categories { -fx-hgap: 7; -fx-vgap: 3; } -#tags .label { - -fx-text-fill: white; - -fx-background-color: #3e7b91; +#tags .label, #categories .label { + -fx-text-fill: #424242; + -fx-background-color: #47DDA7; -fx-padding: 1 3 1 3; -fx-border-radius: 2; -fx-background-radius: 2; -fx-font-size: 11; } + +/* Right Panel Section */ +.title { + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: #333333; + -fx-opacity: 1; + -fx-font-size: 24px; + -fx-font-weight: bold; + -fx-padding: 0 0 10 0; +} + +.total-balance { + -fx-background-color: #E0E0E0; + -fx-padding: 10; + -fx-border-radius: 8; + -fx-background-radius: 8; +} + +.you-owe-text { + -fx-text-fill: red; +} + +.you-are-owed-text { + -fx-text-fill: green; +} + +.filter-btn { + -icon-paint: gray; + -fx-background-color: -icon-paint; + -fx-border-color: -icon-paint; + -size: 24; + -fx-min-height: -size; + -fx-min-width: -size; + -fx-max-height: -size; + -fx-max-width: -size; + + -fx-shape: "M 52.537 80.466 V 45.192 L 84.53 2.999 C 85.464 1.768 84.586 0 83.041 0 H 6.959 C 5.414 0 4.536 1.768 5.47 2.999 l 31.994 42.192 v 43.441 c 0 1.064 1.163 1.719 2.073 1.167 l 11.758 -7.127 C 52.065 82.205 52.537 81.368 52.537 80.466 z"; +} + +.filter-btn:hover { + -icon-paint: linear-gradient(to bottom, gray, blue); +} + +.transaction-card { + -fx-padding: 10 0; +} + +.transaction-month, .transaction-year { + -fx-font-size: 16px; + -fx-text-fill: #515151; +} + +.transaction-name { + -fx-font-size: 18px; +} + +.transaction-description { + -fx-font-size: 14px; + -fx-text-fill: #515151 !important; +} + +.amount { + -fx-font-weight: bold; + -fx-font-size: 14px; +} diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..9f35f92b6b9 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -2,7 +2,6 @@ - @@ -10,51 +9,64 @@ + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + - - - - - + + + + + - - - - - + + + + + + + - - - - - - + + + + + + + - - - - + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index 84e09833a87..178c721c5e7 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -8,29 +8,29 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml index a1bb6bbace8..0e2d182e5b9 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/PersonListPanel.fxml @@ -2,7 +2,6 @@ - - - + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 01b691792a9..84f143cc350 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -2,8 +2,7 @@ - - -