diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000000..dd84ea7824f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 000000000000..48d5f81fa422 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000000..bbcbbe7d6155 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index 823d175eb670..c8e31b0cdb8a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ lib/* *.log *.log.* *.csv -config.json +/config.json src/test/data/sandbox/ preferences.json .DS_Store diff --git a/README.adoc b/README.adoc index b4c8aac388ae..5f1f193d8ec3 100644 --- a/README.adoc +++ b/README.adoc @@ -1,11 +1,11 @@ -= Address Book (Level 4) += ePiggy ifdef::env-github,env-browser[:relfileprefix: docs/] -https://travis-ci.org/se-edu/addressbook-level4[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]] -https://ci.appveyor.com/project/damithc/addressbook-level4[image:https://ci.appveyor.com/api/projects/status/3boko2x2vr5cc3w2?svg=true[Build status]] -https://coveralls.io/github/se-edu/addressbook-level4?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master[Coverage Status]] -https://www.codacy.com/app/damith/addressbook-level4?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level4&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]] -https://gitter.im/se-edu/Lobby[image:https://badges.gitter.im/se-edu/Lobby.svg[Gitter chat]] +https://travis-ci.org/CS2103-AY1819S2-W17-4/main[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]] +https://ci.appveyor.com/project/rahulb99/main-3fxt5/branch/master[image:https://ci.appveyor.com/api/projects/status/lli2h4t2ngcwq0ky/branch/master?svg=true[Build Status]] +https://coveralls.io/github/CS2103-AY1819S2-W17-4/main?branch=master[image:https://coveralls.io/repos/github/CS2103-AY1819S2-W17-4/main/badge.svg?branch=master[Coverage Status]] +https://www.codacy.com/app/rahulb99/main?utm_source=github.com&utm_medium=referral&utm_content=CS2103-AY1819S2-W17-4/main&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/678fd7d82cbd4e07a7ca899447c96d45[Codacy Badge]] +https://app.netlify.com/sites/flamboyant-jennings-fec7ca/deploys[image:https://api.netlify.com/api/v1/badges/bb4108e1-6558-4a6c-aece-24ee9561ce0e/deploy-status[Netlify Status]] ifdef::env-github[] image::docs/images/Ui.png[width="600"] @@ -15,24 +15,23 @@ ifndef::env-github[] image::images/Ui.png[width="600"] endif::[] -* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as the main programming language. -* It is *written in OOP fashion*. It provides a *reasonably well-written* code example that is *significantly bigger* (around 6 KLoC)than what students usually write in beginner-level SE modules. -* What's different from https://github.com/se-edu/addressbook-level3[level 3]: -** A more sophisticated GUI that includes a list panel and an in-built Browser. -** More test cases, including automated GUI testing. -** Support for _Build Automation_ using Gradle and for _Continuous Integration_ using Travis CI. +Have you ever felt that regular expense trackers are too complicated? We understand! + +Introducing to you, *_ePiggy_* - a simplified expense tracker application targeted towards the needs of students! We're here to make managing money a breeze for even the youngest students. + +Interested to know more? Visit our <> to get started! + + +ePiggy is an open source project that welcomes contributions from the community. If you would like to contribute, visit the <> to get started! == Site Map * <> * <> -* <> * <> * <> == Acknowledgements +* The original source of the code (https://github.com/se-edu/addressbook-level4[AddressBook-Level4]) was created by the https://github.com/se-edu/[SE-EDU] initiative. * Some parts of this sample application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by _Marco Jakob_. * Libraries used: https://github.com/TestFX/TestFX[TextFX], https://github.com/FasterXML/jackson[Jackson], https://github.com/google/guava[Guava], https://github.com/junit-team/junit5[JUnit5] diff --git a/_reposense/config.json b/_reposense/config.json new file mode 100644 index 000000000000..a096c3df3f77 --- /dev/null +++ b/_reposense/config.json @@ -0,0 +1,32 @@ +{ + "authors": + [ + { + "githubId": "kev-inc", + "displayName": "KEV...ONG", + "emails": "kevinchanxy@gmail.com", + "authorNames": ["kev-inc", "Kevin"] + }, + { + "githubId": "pdnm", + "displayName": "PHA...INH", + "authorNames": ["pdnm", "Phan Duc Nhat Minh"] + }, + { + "githubId": "rahulb99", + "displayName": "RAH...AID", + "authorNames": ["rahulb99", "Rahul Baid"] + }, + { + "githubId": "tehwenyi", + "displayName": "TEH... YI", + "authorNames": ["tehwenyi"] + }, + { + "githubId": "yunjun199321", + "displayName": "WU ...JUN", + "emails": "yunjun199321@gmail.com", + "authorNames": ["yunjun199321", "Wu Yunjun"] + } + ] +} diff --git a/build.gradle b/build.gradle index 4f2949b6e774..143679ada4bf 100644 --- a/build.gradle +++ b/build.gradle @@ -77,7 +77,8 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'ePiggy.jar' + baseName = 'ePiggy' destinationDir = file("${buildDir}/jar/") } @@ -202,8 +203,8 @@ asciidoctor { idprefix: '', // for compatibility with GitHub preview idseparator: '-', 'site-root': "${sourceDir}", // must be the same as sourceDir, do not modify - 'site-name': 'AddressBook-Level4', - 'site-githuburl': 'https://github.com/se-edu/addressbook-level4', + 'site-name': 'ePiggy', + 'site-githuburl': 'https://github.com/CS2103-AY1819S2-W17-4/main', 'site-seedu': true, // delete this line if your project is not a fork (not a SE-EDU project) ] diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index e647ed1e715a..0dbb1c03ca7b 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -4,53 +4,76 @@ :imagesDir: images :stylesDir: stylesheets -AddressBook - Level 4 was developed by the https://se-edu.github.io/docs/Team.html[se-edu] team. + -_{The dummy content given below serves as a placeholder to be used by future forks of the project.}_ + -{empty} + -We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. +ePiggy is developed by https://github.com/CS2103-AY1819S2-W17-4[A+ for 2103T] team. +We are a team of Year 2 Computer Science undergraduates based in http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. == Project Team -=== John Doe -image::damithc.jpg[width="150", align="left"] -{empty}[http://www.comp.nus.edu.sg/~damithch[homepage]] [https://github.com/damithc[github]] [<>] +=== Phan Duc Nhat Minh +image::pdnm.png[width="150", align="left"] +{empty}[https://github.com/pdnm[github]] [<>] -Role: Project Advisor +Role: Team Leader, Developer + +Responsibilities: + +1. Basic features + +2. Java expert + ''' -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] +=== Teh Wen Yi +image::tehwenyi.png[width="150", align="left"] +{empty}[https://github.com/tehwenyi[github]] [<>] + +Role: Developer -Role: Team Lead + -Responsibilities: UI +Responsibilities: + +1. `budget` feature + +2. Documentations + +3. Testing + ''' -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] +=== Rahul Baid +image::rahulb99.png[width="150", align="left"] +{empty}[https://github.com/rahulb99[github]] [<>] + +Role: Developer -Role: Developer + -Responsibilities: Data +Responsibilities: + +1. `find` feature + +2. `sort` feature + +3. Refactoring codebase + +4. Other minor features include `list`, `reverseList` and `help`. + +5. Testing ''' -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Wu Yunjun +image::yunjun199321.png[width="150", align="left"] +{empty}[https://github.com/yunjun199321[github]] [<>] -Role: Developer + -Responsibilities: Dev Ops + Threading +Role: Developer + +Responsibilities: + +1. `report` feature + +2. `autocomplete` feature + +3. Bug Finder + +4. Testing + ''' -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] +=== Kevin +image::kev-inc.png[width="150", align="left"] +{empty}[https://github.com/kev-inc[github]] [<>] + +Role: Developer -Role: Developer + -Responsibilities: UI +Responsibilities: + +1. `allowance` feature + +2. `goal` feature + +3. `savings` feature + +4. Storage + +5. Testing + ''' diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc index 5de5363abffd..bf7a16cfee76 100644 --- a/docs/ContactUs.adoc +++ b/docs/ContactUs.adoc @@ -2,6 +2,11 @@ :site-section: ContactUs :stylesDir: stylesheets -* *Bug reports, Suggestions* : Post in our https://github.com/se-edu/addressbook-level4/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. +* *Bug reports, Suggestions* : Post in our https://github.com/CS2103-AY1819S2-W17-4/main/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. * *Contributing* : We welcome pull requests. Follow the process described https://github.com/oss-generic/process[here] -* *Email us* : You can also reach us at `damith [at] comp.nus.edu.sg` +* *Email us* : You can also reach us at +** Kevin: kevin.chan@u.nus.edu +** Minh: minh@u.nus.edu +** Rahul: rahul.baid@u.nus.edu +** Wen Yi: wenyiteh@u.nus.edu +** Yun Jun: e0191521@u.nus.edu diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 8b92d5fb7e62..40d893b75693 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,8 +1,9 @@ -= AddressBook Level 4 - Developer Guide += ePiggy - Developer Guide :site-section: DeveloperGuide :toc: :toc-title: -:toc-placement: preamble +:toclevels: 3 +:toc-placement: macro :sectnums: :imagesDir: images :stylesDir: stylesheets @@ -13,12 +14,46 @@ ifdef::env-github[] :warning-caption: :warning: :experimental: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master +:repoURL: https://github.com/CS2103-AY1819S2-W17-4/main -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +image::developerguide.png[width="600"] + +By: `Team A+ for 2103T`      Since: `Feb 2019`      Last Updated: `April 2019`      Licence: `MIT` + +== Introduction + +Welcome to *_ePiggy_*! *ePiggy* is a desktop application designed to inculcate good spending habits in students +through allowing them to track their finances. It includes everything from tracking expenses, managing budgets +to setting goals. + + +This developer guide is a self-contained resource designed to align potential developers to a common vision. It guides +developers of all levels, allowing them to learn more about the workings behind the scenes and how to make use of them effectively. + +If you want to learn how you can make *_ePiggy_* even better, start here! + + +image::callouts.png[width="175"] + +Callouts are boxes with icons which points out some information. These are the 3 callouts used throughout this developer guide: + +[NOTE] +This represents a *note*. A note indicates important, additional information. Be sure to read these notes as they might be applicable to you! + +[TIP] +This represents a *tip*. A tip denotes something that is often handy, and good for you to know. Tips are often less crucial, and you can choose to skip them. + +[WARNING] +This represents a *warning*. A warning denotes something of crucial importance, and you should be extremely cautious when reading the statement. + +{empty} + + +*In this Developer Guide:* + +toc::[] == Setting up +image::developerguidesettingup.png[width="350"] + === Prerequisites . *JDK `9`* or later @@ -82,7 +117,7 @@ If you plan to develop this fork as a separate product (i.e. instead of contribu . Replace the URL in the attribute `repoURL` in link:{repoURL}/docs/DeveloperGuide.adoc[`DeveloperGuide.adoc`] and link:{repoURL}/docs/UserGuide.adoc[`UserGuide.adoc`] with the URL of your fork. -==== Setting up CI +=== Setting up CI Set up Travis to perform Continuous Integration (CI) for your fork. See <> to learn how to set it up. @@ -105,6 +140,8 @@ When you are ready to start coding, == Design +image::developerguidedesign.png[width="350"] + [[Design-Architecture]] === Architecture @@ -146,10 +183,10 @@ image::LogicClassDiagram.png[width="800"] [discrete] ==== How the architecture components interact with each other -The _Sequence Diagram_ below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The _Sequence Diagram_ below shows how the components interact with each other for the scenario where the user issues the command `deleteExpense 1`. -.Component interactions for `delete 1` command -image::SDforDeletePerson.png[width="800"] +.Component interactions for `deleteExpense 1` command +image::SDforDeleteExpense.png[width="800"] The sections below give more details of each component. @@ -180,17 +217,18 @@ image::LogicClassDiagram.png[width="800"] *API* : link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] -. `Logic` uses the `AddressBookParser` class to parse the user command. +. `Logic` uses the `EPiggyParser` class to parse the user command. . This results in a `Command` object which is executed by the `LogicManager`. -. The command execution can affect the `Model` (e.g. adding a person). +. The command execution can affect the `Model` (e.g. adding a expense). . The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. . In addition, the `CommandResult` object can also instruct the `Ui` to perform certain actions, such as displaying help to the user. -Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` API call. +Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("de 1")` API call. -.Interactions Inside the Logic Component for the `delete 1` Command -image::DeletePersonSdForLogic.png[width="800"] +.Interactions Inside the Logic Component for the `de 1` Command +image::DeleteExpenseSdForLogic.png[width="800"] +// tag::model[] [[Design-Model]] === Model component @@ -202,15 +240,21 @@ image::ModelClassDiagram.png[width="800"] The `Model`, * stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. -* exposes an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the ePiggy data. +* exposes an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* exposes an unmodifiable `ObservableList` that can be 'observed' +* exposes an unmodifiable `ObservableValue` that can be 'observed' * does not depend on any of the other three components. [NOTE] -As a more OOP model, we can store a `Tag` list in `Address Book`, which `Person` can reference. This would allow `Address Book` to only require one `Tag` object per unique `Tag`, instead of each `Person` needing their own `Tag` object. An example of how such a model may look like is given below. + +As a more OOP model, we store an `Item` list in `ePiggy`, which `Expense` can reference to. +This allows `ePiggy` to only require one `Item` object per unique `Item`, +instead of each `Expense` needing their own `Item` object. An example of how such a model may look like is given below. + + image:ModelClassBetterOopDiagram.png[width="800"] +// end::model[] + [[Design-Storage]] === Storage component @@ -222,7 +266,7 @@ image::StorageClassDiagram.png[width="800"] The `Storage` component, * can save `UserPref` objects in json format and read it back. -* can save the Address Book data in json format and read it back. +* can save the ePiggy data in json format and read it back. [[Design-Commons]] === Common classes @@ -231,98 +275,711 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa == Implementation -This section describes some noteworthy details on how certain features are implemented. +image::developerguideimplementation.png[width="350"] + +This section describes some noteworthy details on how certain features are implemented. It is split into 6 sections: +_basic_, _find and sort_, _budget_, _goal_, _report_ and _miscellaneous_. + +=== Basic Features +// tag::ae[] +==== Add expense feature +*_ePiggy_* allows users to add an expense record to the system. +To use this feature, users need to enter the `addExpense` command, +with fields `name` and `amount`, and optional fields `date` and `tag`. +If `date` is not specified, the current date will be used. + +* `addExpense n/Chicken rice set $/5 t/Food d/21/02/2019` + +The command above adds an expense record for a $5 chicken rice set, tagged as Food with the date as 21/02/2019. + + +===== Current Implementation +The figure below shows the sequence diagram for the `addExpense` command: + +.Add Expense Sequence Diagram +image::AddExpenseSequenceDiagram.png[widith="800] + +The command is recognized by `parseCommand` and an `AddExpenseCommandParser` is created, +which is used to parse the command with the separated input arguments. +An `Expense` is created from the arguments, which is used to create an `AddExpenseCommand`. + +`AddExpenseCommand` is then executed by `Logic` and it calls `addExpense` of the model, which +add the `Expense` into the `ExpenseList`. +// end::ae[] + +// tag::ee[] +==== Edit expense feature +*_ePiggy_* allows users to edit an existing expense record in the expense list. +To use this feature, users need to enter the `editExpense` command, +with the `INDEX` (in the displaying list) of the expense to be edited, along with +the fields that are to be modified. + +* `editExpense 1 n/Pen $1 t/Supplies` + +The command above edits the name, cost and tag of the +first expense in the displaying list to ‘Pen’, ‘$1’ and ‘Supplies’ respectively. + + +The command is recognized by `parseCommand` and an `EditExpenseCommandParser` is created, +Then it create an `EditExpenseCommand` object with the `EditExpenseDescriptor` specifying the fields to be edited. + +The command object is then executed by `Logic` and it calls `setExpense` of the model, which +replaces the old `Expense` by the edited one. +// end::ee[] + +// tag::de[] +==== Delete expense feature +*_ePiggy_* allows users to remove an expense record from the expense list. +To use this feature, users need to enter the `deleteExpense` command, +with the `INDEX` (in the displaying list) of the expense to be deleted. + +* `deleteExpense 1` + +The command above deletes the first expense in the displaying expense list. + + +The command is recognized by `parseCommand` and an `DeleteExpenseCommandParser` is created, +Then it create an`DeleteExpenseCommand` object with the parsed index. + +The command object is then executed by `Logic` and it calls `deleteExpense` of the model, which +removes the `Expense` from the `ExpenseList`. +// end::de[] + + +// tag::aa[] +==== Add allowance feature +*_ePiggy_* allows the tracking of allowances by adding +allowances into the expenses list. This helps in calculating the net amount of money the user has. + +Similar to expenses, the user will have to specify the `name` and the `amount`, +with optional fields for `date`, and `tag`. + +* `addAllowance n/From Dad $/30 d/04/02/2019 t/School` + +The command above adds a new allowance entry of `$30` with the title `From Dad`, on `4th February 2019` +with the `School` tag. + + +[NOTE] +`Allowance` and `Expense` are shown in the same list because we made `Allowance` a subclass of +`Expense`. We consider an `Allowance` to be a type of negative expense which makes calculating +the total savings in *ePiggy* easier. + +===== Current Implementation +The figure below shows the sequence diagram for the `addAllowance` command: + +.Add Allowance Sequence Diagram +image::AddAllowanceSequenceDiagram.png[widith="800] + +The command is recognised by `parseCommand` and an `AddAllowanceCommandParser` is created, +which is used to separate the arguments into their respective fields. +A new `Item` is created and this `Item` is used in creating the +`Allowance` object, which is used to create an `AddAllowanceCommand`. + +`AddAllowanceCommand` is then executed by `Logic` and `addAllowance()` is called from the model, +which adds the `Allowance` into the `ExpenseList`. + +===== Design Considerations +*Aspect: Should `Allowance` be a subclass of `Expense`?* + +* *Alternative 1 (current choice):* `Allowance` should be a subclass of `Expense`. +** Pros: `Allowance` and `Expense` are very similar, the only difference being whether to +add or subtract when calculating total savings. Extending `Expense` would reduce a lot of +repeated code. This way we can place allowances and expenses in the same list. +** Cons: This `Allowance` IS-A `Expense` relationship does not necessarily hold in real life. + +* *Alternative 2:* `Allowance` and `Expense` should both be subclasses of the same parent class +** Pros: Makes more logical sense. +** Cons: Requires more code in determining which object is which. +// end::aa[] + +// tag::ea[] +==== Edit allowance feature +*_ePiggy_* allows users to edit previously entered allowances. +It requires the `INDEX` of the allowance, as well as the fields to be modified. + +* `editAllowance 1 $/50 d/06/03/2019` + +_The command above edits the allowance with index `1` on the allowance and expense list, +to change the amount to `$50` and the date to `06/03/2019`._ + +[NOTE] +While allowances and expenses are shown on the same list, the `INDEX` specified must belong +to an `Allowance`, otherwise the command would not work. + +===== Current Implementation +The command is recognised by `parseCommand` and an `EditAllowanceCommandParser` +is created. It then creates an `EditAllowanceCommand` object with an +`EditAllowanceDescriptor` specifying the fields to be edited. +`EditAllowanceCommand` is the executed by `Logic` and calls `setExpense()` +of the model, which replaces the old `Allowance` with the updated one. + +Since only allowances can be edited with the `editAllowance` command, the `EditAllowanceCommand` +will first check if the `INDEX` specified points to an `Allowance`. If `INDEX` points to an +`Expense`, a `CommandException` will be thrown. + +===== Design Considerations +*Aspect: Should the edit command for `Allowance` and `Expense` be combined?* + +* *Alternative 1 (current version):* Edits for `Allowance` and `Expense` should have their own +dedicated commands. +** Pros: It is easier for the user to understand the function of the command. +** Cons: More commands means more coding required. The long list of allowance and expense +specific commands also means a longer list of commands. + +* *Alternative 2:* Combine `editAllowance` and `editExpense` commands into 1 `edit` command. +** Pros: Easier to implement, lesser commands for the user to remember. +** Cons: Using `edit` might be too vague, as it does not specify what it is editing (could be +mistaken with editing budget or goal). + +// end::ea[] +// tag::da[] +==== Delete allowance feature +*_ePiggy_* allows users to delete an allowance from the list using the +`deleteAllowance` command by specifying the `INDEX` of the allowance to be deleted. + +* `deleteAllowance 2` + +The command above deletes the allowance with index `2` on the allowance and expense list. + + +===== Current Implementation +The figure below shows the sequence diagram for the `deleteAllowance` command: + +.Delete Allowance Sequence Diagram +image::DeleteAllowanceSequenceDiagram.png[width="800"] + +The command is recognised by `parseCommand` and an `DeleteAllowanceCommandParser` +is created. Then, it creates a `DeleteAllowanceCommand` object with the given +`INDEX`. `DeleteAllowanceCommand` is then executed by `Logic` and it calls +`deleteAllowance()` of the model, which removes the `Allowance` from the +`ExpenseList`. +// end::da[] + +//tag::reverse[] +==== Reverse list feature +*_ePiggy_* allows the user to list the expenses in any order they want by using the 'list' feature and this `reverseList` feature. +This makes viewing of expenses more customisable and convenient for the user. + +* `reverseList` + +The command above lists the expenses in reverse. + + +===== Current Implementation +Given below is a UML sequence diagram of how the `reverseList` command works along with a step-by-step +explanation. + + +.Reverse List Sequence Diagram +image::rLuml.png[witdh="750"] + +Step 1: User enters `rl`. The command is received by `EPiggyParser` which then creates `ReverseListCommand` object +and returned to `LogicManager`. + + +Step 2: `LogicManager` calls `ReverseListCommand#execute()`,which then calls `Model#reverseFilteredExpensesList()` +method to set the comparator for sorting. + + +Step 3: `Model#reverseFilteredExpensesList()` then calls `EPiggy#reverseExpenseList()` method. `EPiggy#reverseExpenseList` +then calls `ExpenseList#reverse`. `ExpenseList#reverse` then calls the static `FXCollections#reverse` +method to reverse the `internalList`. + + +Step 4: Expense List panel is updated by expenses listed in reverse order. A `CommandResult` is then created and +returned to `LogicManager`. + +//end::reverse[] + +=== Find and Sort Features +//tag::findexpense[] +==== Find feature +*_ePiggy_* allows the user to filter specific expenses by entering single or multiple keywords. Expenses that satisfy +all the keywords are displayed in the expense list panel. Furthermore, it searches for words similar to the user's +input, considering that the user might have made a typing error, by applying the concept of _Levenshtein distance_ fixed by a +upper bound. + +* `find n/Nasi Lemak $/4.00 d/01/04/2019 t/lunch` + +The above command finds expense(s) with the `Name` Nasi Lemak, `Cost` of $4.00, purchased on +`Date` 1st April, 2019 and tagged with `Tag` lunch. + +[NOTE] +`Date` format is `dd/MM/yyyy`. + +Empty input keywords are not allowed and a appropriate error will be shown. + +[TIP] +All keywords in this command are optional, provided that there is at least one input keyword. For +example, suppose we want to filter out all `Expense` s with `Cost` between $1 and $10.5 (both inclusive), +then the command should be just `find $/1:10.5`. +Similarly for other type of keywords. + +===== Current Implementation +Given below is a UML sequence diagram of how the `find` command works along with a step-by-step +explanation. + +.Find Sequence Diagram +image::fEuml.png[witdh="750"] + +Step 1: User enters `find n/Nasi Lemak`. The command is +received by `EPiggyParser` which then creates `FindCommandParser` object and and calls +`FindCommandParser#parse()` method. + + +Step 2: `FindCommandParser#parse()` first checks if input is invalid and throws a +`ParseException` otherwise it calls `ArgumentTokenizer#tokenize()` to tokenize the `String` input + into keywords and store them in an `ArgumentMultimap` Object. + + +Step 3: `FindCommandParser#parse()` method then creates an `ExpenseContainsKeywordsPredicate` +Object. It implements `Predicate` interface. It performs the filtering of expenses. + + +Step 4: A `FindCommand` object is created with `ExpenseContainsKeywordsPredicate` as parameter and +returned to `LogicManager`. + + +Step 5: `LogicManager` calls `FindCommand#execute()`,which then calls `Model#updateFilteredExpenseList()` +method to update the predicate of `FilteredList`. `FilteredList` now contains only a set of +expenses which was filtered by the new predicate. + + +Step 6: Expense List panel is updated by filtered set of expenses. A `CommandResult` is then created and +returned to `LogicManager`. + +===== Design considerations +This feature can be implemented in different ways depending on how the target expenses are found. + +*Aspect: How should the expenses be filtered?* + +* *Alternative 1:* Storing expenses as and when they are added into separate files. + +** Pros: More efficient as there is no need to check for ALL expenses. + +** Cons: Need to change the original architecture of storage. + +* *Alternative 2 (current choice):* Search through all the expenses and find the ones with matching keywords. + +** Pros: : Easy to implement as there is no need to change original architecture. + +** Cons: Will take more time as it will search through large number of expenses. + + +//end::findexpense[] + +//tag::sortexpense[] +==== Sort feature +*_ePiggy_* allows the user to sort expenses (and allowances) by entering the keyword (name, cost or date) to determine +the sorting sequence. By default, a new expense/allowance entry is placed at the bottom of the list (`ExpensePanel`). This +sorting mechanism allows the user to sort the expenses/allowances accordingly. + +* `sort n/` + +The above command sorts expense(s) according to their `Name` in a lexicographical order. + + +[NOTE] +Empty user input is not allowed and a appropriate error will be shown. Similarly, multiple keywords (`Prefix`) are also not allowed. + +===== Current Implementation +Given below is a UML sequence diagram of how the `sort` command works along with a step-by-step +explanation. + + +.Sort Sequence Diagram +image::sEuml.png[witdh="750"] + +Step 1: User enters `sort $/`. The command is received by `EPiggyParser` which then creates `SortCommandParser` object +and calls `SortCommandParser#parse()` method. + + +Step 2: `SortCommandParser#parse()` first checks if input is invalid and throws a + `ParseException` otherwise it calls `ArgumentTokenizer#tokenize()` to tokenize the `String` input + into keywords and store them in an `ArgumentMultimap` Object. + + +Step 3: `SortCommandParser#arePrefixesPresent` method is then used to determine the input `Prefix`, and depending on the `Prefix`, +appropriate Comparator object is created. In this case, `Prefix` is `$/`, so a new `CompareExpensesByCost` object is created. +It implements the `java.util.Comparator` interface. + + +Step 4: A `SortCommand` object is created with `CompareExpensesByCost` object as a parameter and +returned to `LogicManager`. + + +Step 5: `LogicManager` calls `SortCommand#execute()`,which then calls `Model#sortExpenses()` +method to set the comparator for sorting. + + +Step 6: `Model#sortExpenses()` then calls `EPiggy#sortExpense` method with Comparator object as parameter. `EPiggy#sortExpense` +then calls `ExpenseList#sort` to set the comparator of `internalList`. `ExpenseList#sort` then calls the static `FXCollections#sort` +method to sort the `internalList`. + + +Step 7: `SortCommand#execute()`,which then calls `Model#updateFilteredExpenseList()` + method to update the predicate of `FilteredList`. `FilteredList` now contains all + expenses in sorted order. + + +Step 8: Expense List panel is updated by sorted expenses. A `CommandResult` is then created and +returned to `LogicManager`. + +//end::sortexpense[] +=== Budget Features + +==== Add budget feature + +// tag::addbudget[] + +*_ePiggy_* allows users to add new budgets to monitor their expenses within a user specified period of time. This command +requires users to specify the `amount`, `start date` and `time period (in days)` of the budget in the command. + + +* `addBudget $/500 p/31 d/01/03/2019` + +The command above adds a budget with `$500` which starts on `1st March 2019` and lasts for `31` days. + + +The budget will compute the end date and provide a status based on the local date. +The status will include whether the budget is an old, current or future budget, as well as the `remaining amount` until the budget is exceeded and `remaining days` till the end of the budget. +This is so that users are aware about how much they have spent. + + +Adding of overlapping budgets are not allowed in ePiggy. +[NOTE] +A `budget` is considered overlapping if *any* of the dates *in between (inclusive)* one `budget`’s start *and* end dates is the *same* as the dates *in between (inclusive)* another `budget`’s start *and* end dates. + +===== Current Implementation +Given below is a sequence diagram of how the `addBudget` command works: + + +.Add Budget Sequence Diagram +image::addBudgetSequenceDiagram.png[width="800"] + +The command is first recognised by `parseCommand` and an `AddBudgetCommandParser` is created, which is used to separate the +arguments into their respective fields. A new `Budget` is created, and is used to create an `AddBudgetCommand`. + +As long as the `Budget` object created does not overlap with any existing `Budget` objects, the `Budget` will be added +with `Model#addBudget()` and saved into the ePiggy storage. + + +*Example usage scenario:* + +1. User launches application and enters `addBudget $/500 p/31 d/01/03/2019`. + +2. `AddBudgetCommandParser` takes in the arguments and parses the command to create the appropriate `Budget`. + +3. The `AddBudgetCommand` is passed back to the `LogicManager`, and the method `execute()` is called. The `Budget` is then added to the model. + +===== Design Considerations +*Aspect: What user input should `addBudget` require?* + + +* *Alternative 1 (current choice)*: the `addBudget` command requires the `amount`, `start date` and `time period (in days)` of the budget. +** Pros: Easy to make recurring daily, weekly or annual budgets. +** Cons: If users have the start date and end date in mind, they will have to manually calculate the period between the dates and input that instead of the end date. + +* *Alternative 2* : the `addBudget` command requires the `amount`, `start date` and `end date` of the budget. +** Pros: Easy to make recurring monthly budgets. +** Cons: Could cause users to miss out on certain dates if they want budgets that are recurring (eg. sets a budget from 1st March to 30th March and 1st April to 30th April – 31st March is missed out). +// end::addbudget[] + +==== Edit budget feature + +// tag::editbudget[] +*_ePiggy_* allows the user to edit the `current budget`, with any of the specified parameters in `addBudget` (above). + + +* `editBudget $/300` +* `editBudget $/400 p/7` + +The above commands edit the current budget to $300 and $400 with a period of 7 days respectively. + +Similar to the `addBudget` command, budgets’ dates should not overlap each other. Hence, the budget cannot be edited +such that the edited budget overlaps with another budget. + + +[NOTE] +A `budget` is considered overlapping if *any* of the dates *in between (inclusive)* one `budget`’s start *and* end dates is the +*same* as the dates *in between (inclusive)* another `budget`’s start *and* end dates. + +[TIP] +If the current budget is edited such that it is no longer a current budget, it can no longer be edited. Hence, users +should delete that budget and add a new budget using the `addBudget` command should they wish to edit that budget. + +===== Current Implementation +The command’s current implementation uses parts of the legacy implementation to update the budget. The arguments are first parsed into +`EditBudgetCommandParser`, which separates the arguments into their respective fields. + +An `EditBudgetDescriptor` object is then created to temporarily hold this new information. + + +[NOTE] +The prefixes applicable to `editBudget` are `$/`, `p/` and `d/`. At least one of them must +follow the `editBudget` command word. + +Afterwards, a `budget` object is created from the `EditBudgetDescriptor` object. Then, the `budget` object is passed into ePiggy +through `Model.#setCurrentBudget()`, which will replace the current `budget` with the new `budget` passed in. + +Since only the current `budget` can be edited, the `editBudget` command will first check if a current `budget` is present in `ePiggy`’s +`budgetList` through `Model#getCurrentBudgetIndex()`. If the current `budget` does not exist, the command will feedback to the user that the +command entered is invalid. + + +===== Design Considerations +*Aspect: Should we use a boolean `hasCurrentBudget` method or use the `index` of the current `budget` to verify if a +current `budget` exists?* + + +* *Alternative 1* (current choice): +The `index` of the current `budget` is returned to the `editBudgetCommand`. If the returned integer is `-1`, it means that there is no current `budget` +present. The index is then used to retrieve the current budget. + +** Pros: No additional method implementations required. The methods `Model#getFilteredBudgetList().get()` are sufficient to get the current budget. + +** Cons: Calculations are done in the `editBudget` command’s `execute` method. + +* *Alternative 2*: + +Using a boolean `hasCurrentBudget` method to check if a current `budget` exists in `budgetList`, then another `getCurrentBudget` method to get the current `budget`. + +** Pros: Code will be written in `ePiggy` rather than at `editBudget` command and can be easily used for other commands. + +** Cons: Will need to implement additional methods. Reduces the abstraction has the current `budget` is exposed to the entire project as it is a public method. + +After much consideration, we decided to choose option 1 as other commands should not need to access the current `budget` specifically. It will be better +to have a greater level of abstraction. + +// end::editbudget[] + +==== Delete budget feature + +// tag::deletebudget[] + +*_ePiggy_* allows the user to delete any budget, using the displayed `index` of the specific budget. + +* `deleteBudget 2` + +The above command deletes the `Budget` with the displayed `index` of *2*. + +The `Budget` to be deleted is identified by its displayed `index` and subsequently deleted. + + +===== Current Implementation +Given below is a sequence diagram of how the `deleteBudget` command works: + + +.Delete Budget Sequence Diagram +image::DeleteBudgetSequenceDiagram.png[width="800"] + +The command’s current implementation retrieves the `budgetList` from `ePiggy` and removes the +budget at the zero-based version of the displayed `index`. + +A `listener` has been added to `budgetList`, so the order in which the budgets are displayed is the same +as the order of the budgets in `budgetList`. Furthermore, the indexes are unique. + +Hence, if the `index` input by the user is negative or greater than the size of `budgetList`, this would indicate that the budget specified does not exist. The user will receive a feedback that the `index` specified is invalid. +// end::deletebudget[] + +=== Goal Feature + +// tag::setGoal[] +==== Set goal feature +*_ePiggy_* allows users to set a savings goal that they would like to save up to. + +It requires the user to specify the name of the goal, as well as the amount they would like to hit. + +* `setGoal n/Nintendo Switch $/499` + +The command above sets a goal with the name `Nintendo Switch` and with the amount `$499` + +===== Current Implementation +Given below is a sequence diagram of how the `setGoal` command works: + +.Set Goal Sequence Diagram +image::setGoalSequenceDiagram.PNG[width="800"] + +The command is first parsed into `SetGoalCommandParser`, which separates the arguments into their respective fields. A new `Goal` is created and parsed into `SetGoalCommand`. +`Goal` will then be set with `Model#setGoal()` and saved into the ePiggy `Storage`. + +*Example usage scenario:* + +1. User launches application and enters `setGoal n/Nintendo Switch $/499`. + +2. `SetGoalCommandParser` takes in the arguments and parses the command to create the appropriate `Goal`. + +3. The `SetGoalCommand` is passed back to the `LogicManager`, and the method `execute()` is called. `Goal` is then set to the `model`. + +===== Design considerations +*Aspect: `setGoal` (1 goal) VS `addGoal` (multiple goals)* + +* *Alternative 1 (current choice):* `setGoal` (1 goal) + +** Pros: Easier to implement. Makes ePiggy easier to use. + +** Cons: Limits the user experience by allowing only 1 savings goal. + +* *Alternative 2:* `addGoal` (multiple goals) + +** Pros: Gives user freedom to set more than 1 goal. + +** Cons: Makes ePiggy more complicated, not suitable for younger age groups. + + +// end::setGoal[] + +// tag::report[] +=== Report Feature + +==== Expense report feature +*_ePiggy_* allows users to view the report within a user-input period of time. + +This command requires users to specify the `date`, `month` or `year` of the report in the command. +Given below is a sequence diagram of how the `report` command works: + + +.Report Sequence Diagram +image::reportSequenceDiagram.png[width="800"] + +* `report d/DD/MM/YYYY` + + +The above command shows a report on specified date. + + +* `report d/MM/YYYY` + + +The above command shows a report on specified month. + + +* `report d/YYYY` + + +The above command shows a report on specified year. + + +* `report` + + +The above command shows a completed report from first day of user launches the ePiggy. + + +Commands with different format of tag `d/` will generate a report with different charts. + + +Eg: `report d/21/03/2019` + + +The above report command will generate a report of 21 Mar 2019 with AreaChart. + +[NOTE] +Only last tag `d/` is used to generate a report if multiply of `d/` appear. + +===== Current Implementation + +This section shows the current implementation of the report feature. It also shows the classes or components used in the report feature. + +The command is first parsed into link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/parser/epiggy/ReportCommandParser.java[`ReportCommandParser`], +which separates the arguments into their respective fields. A new link:https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html[`localDate`] +object is created and link:[`type`] of the report are generated according to the date format of `d/`. +The link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/model/Model.java[`model`], +link:https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html[`localDate`] and link:[`type`] are parsed into +link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/ReportCommand.java[`ReportCommand`]. + + +The link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/ReportCommand.java[`reportCommand`] will initialize link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ReportWindow.java[`ReportWindow`] and the method +link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ReportWindow.java[`displayReportController`] of the object +link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ReportWindow.java[`ReportWindow`] will be invoked. + + +The link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ReportWindow.java[`displayReportController`] method will select a specified type of report to display the report. + +*Example usage scenario:* + +1. User launches application and enters `Report d/21/03/2019`. + +2. link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/parser/epiggy/ReportCommandParser.java[`ReportCommandParser`] takes in the arguments and parses the command to create the appropriate +link:https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html[`localDate`]. + +3. The link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ReportWindow.java[`ReportWindow`] is passed back to the +link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/LogicManager.java[`LogicManager`], and the method +link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/LogicManager.java[`execute()`] is called. The +link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ReportWindow.java[`ReportWindow`] is initialized. + +4. The method link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ReportWindow.java[`displayReportController`] is called and report will show. + +===== Design Considerations + +This section shows the design considerations of the report feature. The design considerations are shown below together +with their pros and cons. + +*Aspect: How should we make the report more readable?* + + +* *Alternative 1 (current choice)*: the `report` command uses a chart to display different data of expenses, budgets and allowances. +** Pros: Easy to know how much a user have spend on that date, that month or that year. Easy to compare with previous month or year. +** Cons: The details of the expenses, budgets and allowances cannot show in the chart. + +* *Alternative 2* : Show the records of expenses, budgets and allowances in details line by line. +** Pros: User can know the details of each records. +** Cons: Report feature becomes extra because list command can do the same thing. +// end::report[] + +=== Miscellaneous +// tag::autocomplete[] +==== Auto-complete feature + +*_ePiggy_* allows users to reduce the time spent typing and time spent learning ePiggy's commands, especially because ePiggy +uses the Command Line Interface. The auto-complete feature allows ePiggy to be more user-friendly. + + +Given below is an activity diagram of how the link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/CommandBox.java[`auto-complete`] +feature works: + + +.Auto-Complete Activity Diagram +image::AutocompleteActivityDiagram.png[width="800"] + +This feature first requires users to enter the first few letters of their intended command on the +link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/CommandBox.java[`commandBox`] +of ePiggy. Afterwards, users can simply press the *Tab* key to automatically complete their command. +If the completed command is not the user's intended command, they can delete the command before entering the same letters and pressing *Tab* again. +Another command will show if the letters match it. + + +For example, the user can enter letter 'a', press *Tab* and "addExpense n/ $/ t/ d/ " will show. The user can then +delete the command, enter 'a' again and press *Tab*. The `addBudget` or `addAllowance` command will show. + +[NOTE] +The auto-complete feature compares the last part of the sub-string from the user input to the prefix of command in the +checklist. It is non-case sensitive. + +For example, the user can type " `hello add` " to the commandBox, so the sub-string is "add". "add" is used to compare +with commands in the checklist such as "setBudget", "addExpense", "addAllowance". The two commands "addExpense" and +"addAllowance" will be returned but only one of them will replace "add" in the commandBox. +The commandBox will show either " `hello addExpense n/ $/ t/ d/` " or " `hello addAllowance n/ $/ d/` ". + +===== Current Implementation + +This section shows the current implementation of the auto-complete feature. It also shows the classes or components used in the auto-complete feature. + +The auto-complete function's code is in the link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/CommandBox.java[`CommandBox`] class. +The link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/CommandBox.java[`autoCompleteText()`] method is invoked when user presses *Tab*. The sub-string (last part split by white space) of user input text and a checklist of commands pass into +link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/CommandBox.java[`findString()`]. +link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/CommandBox.java[`findString()`] returns an array of matched commands. One element in the array replaces and shows +in the commandBox. + +*Example usage scenario:* + +1. User launches application and enters `addE`. + +2. link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/CommandBox.java[`autoCompleteText()`] is invoked. + +3. link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/CommandBox.java[`findString()`] takes in the arguments and returns an array of matched commands. + +4. link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/CommandBox.java[`findString()`] forms a new string using the user's input. +One element in the return array is appended at the end of the new string. This new string replaces the user's input of `addE` and shows in the commandBox. + +===== Design Considerations + +This section shows the design considerations of the auto-complete feature. The design considerations are shown below together +with their pros and cons. + +*Aspect: How should we implement such a function in the UI?* + + +* *Alternative 1 (current choice)*: The matched command shows after users press *Tab*. +** Pros: Easy to implement. No third party library is used, which means that all behaviours of this feature is under +control and no extra learning cost is needed. +** Cons: The auto-complete command matched may not be what the user wants. Users will then need to delete the command +and invoke the function again. + +* *Alternative 2*: A dropdown list of matched commands shows as the user is typing. +** Pros: User can see all the matched commands and select one of them. +** Cons: A third party library is needed. However, there is no such library under the MIT licence and hence we are unable +to do this. +// end::autocomplete[] // tag::undoredo[] -=== Undo/Redo feature -==== Current Implementation +==== Undo/Redo feature + +===== Current Implementation -The undo/redo mechanism is facilitated by `VersionedAddressBook`. -It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. +The undo/redo mechanism is facilitated by `VersionedEPiggy`. +It extends `ePiggy` with an undo/redo history, stored internally as an `ePiggyStateList` 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. +* `VersionedEPiggy#commit()` -- Saves the current ePiggy state in its history. +* `VersionedEPiggy#undo()` -- Restores the previous ePiggy state from its history. +* `VersionedEPiggy#redo()` -- Restores a previously undone ePiggy state from its history. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +These operations are exposed in the `Model` interface as `Model#commitEPiggy()`, `Model#undoEPiggy()` and `Model#redoEPiggy()` respectively. Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +Step 1. The user launches the application for the first time. The `VersionedEPiggy` will be initialized with the initial ePiggy state, and the `currentStatePointer` pointing to that single ePiggy state. image::UndoRedoStartingStateListDiagram.png[width="800"] -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +Step 2. The user executes `de 5` command to delete the 5th expense in the ePiggy. The `de` command calls `Model#commitEPiggy()`, causing the modified state of the ePiggy after the `de 5` command executes to be saved in the `ePiggyStateList`, and the `currentStatePointer` is shifted to the newly inserted ePiggy state. image::UndoRedoNewCommand1StateListDiagram.png[width="800"] -Step 3. The user executes `add n/David ...` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +Step 3. The user executes `ae n/chick ...` to add a new expense. The `ae` command also calls `Model#commitEPiggy()`, causing another modified ePiggy state to be saved into the `ePiggyStateList`. image::UndoRedoNewCommand2StateListDiagram.png[width="800"] [NOTE] -If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +If a command fails its execution, it will not call `Model#commitEPiggy()`, so the ePiggy state will not be saved into the `ePiggyStateList`. -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +Step 4. The user now decides that adding the expense was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoEPiggy()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous ePiggy state, and restores the ePiggy to that state. image::UndoRedoExecuteUndoStateListDiagram.png[width="800"] [NOTE] -If the `currentStatePointer` is at index 0, pointing to the initial address book state, then there are no previous address book 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. +If the `currentStatePointer` is at index 0, pointing to the initial ePiggy state, then there are no previous ePiggy states to restore. The `undo` command uses `Model#canUndoEPiggy()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo. The following sequence diagram shows how the undo operation works: image::UndoRedoSequenceDiagram.png[width="800"] -The `redo` command does the opposite -- it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +The `redo` command does the opposite -- it calls `Model#redoEPiggy()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the ePiggy to that state. [NOTE] -If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone address book states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +If the `currentStatePointer` is at index `ePiggyStateList.size() - 1`, pointing to the latest ePiggy state, then there are no undone ePiggy states to restore. The `redo` command uses `Model#canRedoEPiggy()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. -Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +Step 5. The user then decides to execute the command `list`. Commands that do not modify the ePiggy, such as `list`, will usually not call `Model#commitEPiggy()`, `Model#undoEPiggy()` or `Model#redoEPiggy()`. Thus, the `ePiggyStateList` remains unchanged. image::UndoRedoNewCommand3StateListDiagram.png[width="800"] -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. We designed it this way because it no longer makes sense to redo the `add n/David ...` command. This is the behavior that most modern desktop applications follow. +Step 6. The user executes `clear`, which calls `Model#commitEPiggy()`. Since the `currentStatePointer` is not pointing at the end of the `ePiggyStateList`, all ePiggy states after the `currentStatePointer` will be purged. We designed it this way because it no longer makes sense to redo the `ae n/chick ...` command. This is the behavior that most modern desktop applications follow. image::UndoRedoNewCommand4StateListDiagram.png[width="800"] The following activity diagram summarizes what happens when a user executes a new command: -image::UndoRedoActivityDiagram.png[width="650"] +image::UndoRedoActivityDiagram.png[width="800"] -==== Design Considerations +===== Design Considerations -===== Aspect: How undo & redo executes +*Aspect: How undo & redo executes* -* **Alternative 1 (current choice):** Saves the entire address book. +* **Alternative 1 (current choice):** Saves the entire ePiggy. ** 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). +** Pros: Will use less memory (e.g. for `deleteExpense`, just save the expense being deleted). ** Cons: We must ensure that the implementation of each individual command are correct. -===== Aspect: Data structure to support the undo/redo commands +*Aspect: Data structure to support the undo/redo commands* -* **Alternative 1 (current choice):** Use a list to store the history of address book states. +* **Alternative 1 (current choice):** Use a list to store the history of ePiggy states. ** Pros: Easy for new Computer Science student undergraduates to understand, who are likely to be the new incoming developers of our project. -** Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `VersionedAddressBook`. +** Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `VersionedEPiggy`. * **Alternative 2:** Use `HistoryManager` for undo/redo ** Pros: We do not need to maintain a separate list, and just reuse what is already in the codebase. ** Cons: Requires dealing with commands that have already been undone: We must remember to skip these commands. Violates Single Responsibility Principle and Separation of Concerns as `HistoryManager` now needs to do two different things. // end::undoredo[] -// tag::dataencryption[] -=== [Proposed] Data Encryption - -_{Explain here how the data encryption feature will be implemented}_ +==== Logging -// end::dataencryption[] - -=== Logging - -We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels and logging destinations. +We are using the `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels and logging destinations. * The logging level can be controlled using the `logLevel` setting in the configuration file (See <>) * The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level @@ -336,12 +993,13 @@ We are using `java.util.logging` package for logging. The `LogsCenter` class is * `FINE` : Details that is not usually noteworthy but may be useful in debugging e.g. print the actual list instead of just its size [[Implementation-Configuration]] -=== Configuration +==== Configuration Certain properties of the application can be controlled (e.g user prefs file location, logging level) through the configuration file (default: `config.json`). == Documentation +image::developerguidedocumentation.png[width="350"] We use asciidoc for writing documentation. [NOTE] @@ -447,6 +1105,8 @@ The SE-EDU team does not provide support for modified template files. [[Testing]] == Testing +image::developerguidetesting.png[width="350"] + === Running Tests There are three ways to run tests. @@ -494,7 +1154,9 @@ e.g. `seedu.address.logic.LogicManagerTest` * Reason: One of its dependencies, `HelpWindow.html` in `src/main/resources/docs` is missing. * Solution: Execute Gradle task `processResources`. -== Dev Ops +== Development and Operations + +image::developerguidedevops.png[width="350"] === Build Automation @@ -522,7 +1184,7 @@ Here are the steps to create a new release. === Managing Dependencies -A project often depends on third-party libraries. For example, Address Book depends on the https://github.com/FasterXML/jackson[Jackson library] for JSON parsing. Managing these _dependencies_ can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives: +A project often depends on third-party libraries. For example, ePiggy depends on the https://github.com/FasterXML/jackson[Jackson library] for JSON parsing. Managing these _dependencies_ can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives: [loweralpha] . Include those libraries in the repo (this bloats the repo size) @@ -551,14 +1213,14 @@ Each individual exercise in this section is component-based (i.e. you would not [TIP] Do take a look at <> before attempting to modify the `Logic` component. -. Add a shorthand equivalent alias for each of the individual commands. For example, besides typing `clear`, the user can also type `c` to remove all persons in the list. +. Add a shorthand equivalent alias for each of the individual commands. For example, besides typing `clear`, the user can also type `c` to remove all expenses in the list. + **** * Hints ** Just like we store each individual command word constant `COMMAND_WORD` inside `*Command.java` (e.g. link:{repoURL}/src/main/java/seedu/address/logic/commands/FindCommand.java[`FindCommand#COMMAND_WORD`], link:{repoURL}/src/main/java/seedu/address/logic/commands/DeleteCommand.java[`DeleteCommand#COMMAND_WORD`]), you need a new constant for aliases as well (e.g. `FindCommand#COMMAND_ALIAS`). -** link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] is responsible for analyzing command words. +** link:{repoURL}/src/main/java/seedu/address/logic/parser/EPiggyParser.java[`EPiggyParser`] is responsible for analyzing command words. * Solution -** Modify the switch statement in link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser#parseCommand(String)`] such that both the proper command word and alias can be used to execute the same intended command. +** Modify the switch statement in link:{repoURL}/src/main/java/seedu/address/logic/parser/EPiggyParser.java[`EPiggyParser#parseCommand(String)`] such that both the proper command word and alias can be used to execute the same intended command. ** Add new tests for each of the aliases that you have added. ** Update the user guide to document the new aliases. ** See this https://github.com/se-edu/addressbook-level4/pull/785[PR] for the full solution. @@ -567,21 +1229,21 @@ Do take a look at <> before attempting to modify the `Logic` compo [discrete] ==== `Model` component -*Scenario:* You are in charge of `model`. One day, the `logic`-in-charge approaches you for help. He wants to implement a command such that the user is able to remove a particular tag from everyone in the address book, but the model API does not support such a functionality at the moment. Your job is to implement an API method, so that your teammate can use your API to implement his command. +*Scenario:* You are in charge of `model`. One day, the `logic`-in-charge approaches you for help. He wants to implement a command such that the user is able to remove a particular tag from everyone in ePiggy, but the model API does not support such a functionality at the moment. Your job is to implement an API method, so that your teammate can use your API to implement his command. [TIP] Do take a look at <> before attempting to modify the `Model` component. -. Add a `removeTag(Tag)` method. The specified tag will be removed from everyone in the address book. +. Add a `removeTag(Tag)` method. The specified tag will be removed from everyone in the ePiggy. + **** * Hints -** The link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model`] and the link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] API need to be updated. +** The link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model`] and the link:{repoURL}/src/main/java/seedu/address/model/ePiggy.java[`ePiggy`] API need to be updated. ** Think about how you can use SLAP to design the method. Where should we place the main logic of deleting tags? -** Find out which of the existing API methods in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] classes can be used to implement the tag removal logic. link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] allows you to update a person, and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] allows you to update the tags. +** Find out which of the existing API methods in link:{repoURL}/src/main/java/seedu/address/model/ePiggy.java[`ePiggy`] and link:{repoURL}/src/main/java/seedu/address/model/expense/Expense.java[`Expense`] classes can be used to implement the tag removal logic. link:{repoURL}/src/main/java/seedu/address/model/ePiggy.java[`ePiggy`] allows you to update a expense, and link:{repoURL}/src/main/java/seedu/address/model/expense/Expense.java[`Expense`] allows you to update the tags. * Solution -** Implement a `removeTag(Tag)` method in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`]. Loop through each person, and remove the `tag` from each person. -** Add a new API method `deleteTag(Tag)` in link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`]. Your link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`] should call `AddressBook#removeTag(Tag)`. +** Implement a `removeTag(Tag)` method in link:{repoURL}/src/main/java/seedu/address/model/ePiggy.java[`ePiggy`]. Loop through each expense, and remove the `tag` from each expense. +** Add a new API method `deleteTag(Tag)` in link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`]. Your link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`] should call `ePiggy#removeTag(Tag)`. ** Add new tests for each of the new public methods that you have added. ** See this https://github.com/se-edu/addressbook-level4/pull/790[PR] for the full solution. **** @@ -589,12 +1251,12 @@ Do take a look at <> before attempting to modify the `Model` compo [discrete] ==== `Ui` component -*Scenario:* You are in charge of `ui`. During a beta testing session, your team is observing how the users use your address book application. You realize that one of the users occasionally tries to delete non-existent tags from a contact, because the tags all look the same visually, and the user got confused. Another user made a typing mistake in his command, but did not realize he had done so because the error message wasn't prominent enough. A third user keeps scrolling down the list, because he keeps forgetting the index of the last person in the list. Your job is to implement improvements to the UI to solve all these problems. +*Scenario:* You are in charge of `ui`. During a beta testing session, your team is observing how users use your ePiggy application. You realize that one of users occasionally tries to delete non-existent tags from a contact, because the tags all look the same visually, and the user got confused. Another user made a typing mistake in his command, but did not realize he had done so because the error message wasn't prominent enough. A third user keeps scrolling down the list, because he keeps forgetting the index of the last expense in the list. Your job is to implement improvements to the UI to solve all these problems. [TIP] Do take a look at <> before attempting to modify the `UI` component. -. Use different colors for different tags inside person cards. For example, `friends` tags can be all in brown, and `colleagues` tags can be all in yellow. +. Use different colors for different tags inside expense cards. For example, `friends` tags can be all in brown, and `colleagues` tags can be all in yellow. + **Before** + @@ -634,12 +1296,12 @@ image::getting-started-ui-result-after.png[width="200"] ** Modify link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay#handleNewResultAvailableEvent(NewResultAvailableEvent)`] to react to this event appropriately. ** You can write two different kinds of tests to ensure that the functionality works: *** The unit tests for `ResultDisplay` can be modified to include verification of the color. -*** The system tests link:{repoURL}/src/test/java/systemtests/AddressBookSystemTest.java[`AddressBookSystemTest#assertCommandBoxShowsDefaultStyle() and AddressBookSystemTest#assertCommandBoxShowsErrorStyle()`] to include verification for `ResultDisplay` as well. +*** The system tests link:{repoURL}/src/test/java/systemtests/EPiggySystemTest.java[`EPiggySystemTest#assertCommandBoxShowsDefaultStyle() and EPiggySystemTest#assertCommandBoxShowsErrorStyle()`] to include verification for `ResultDisplay` as well. ** See this https://github.com/se-edu/addressbook-level4/pull/799[PR] for the full solution. *** Do read the commits one at a time if you feel overwhelmed. **** -. Modify the link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to show the total number of people in the address book. +. Modify the link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to show the total number of people in the ePiggy. + **Before** + @@ -652,29 +1314,29 @@ image::getting-started-ui-status-after.png[width="500"] **** * Hints ** link:{repoURL}/src/main/resources/view/StatusBarFooter.fxml[`StatusBarFooter.fxml`] will need a new `StatusBar`. Be sure to set the `GridPane.columnIndex` properly for each `StatusBar` to avoid misalignment! -** link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] needs to initialize the status bar on application start, and to update it accordingly whenever the address book is updated. +** link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] needs to initialize the status bar on application start, and to update it accordingly whenever the ePiggy is updated. * Solution -** Modify the constructor of link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to take in the number of persons when the application just started. -** Use link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter#handleAddressBookChangedEvent(AddressBookChangedEvent)`] to update the number of persons whenever there are new changes to the addressbook. +** Modify the constructor of link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to take in the number of expenses when the application just started. +** Use link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter#handleEPiggyChangedEvent(EPiggyChangedEvent)`] to update the number of expenses whenever there are new changes to the addressbook. ** For tests, modify link:{repoURL}/src/test/java/guitests/guihandles/StatusBarFooterHandle.java[`StatusBarFooterHandle`] by adding a state-saving functionality for the total number of people status, just like what we did for save location and sync status. -** For system tests, modify link:{repoURL}/src/test/java/systemtests/AddressBookSystemTest.java[`AddressBookSystemTest`] to also verify the new total number of persons status bar. +** For system tests, modify link:{repoURL}/src/test/java/systemtests/EPiggySystemTest.java[`EPiggySystemTest`] to also verify the new total number of expenses status bar. ** See this https://github.com/se-edu/addressbook-level4/pull/803[PR] for the full solution. **** [discrete] ==== `Storage` component -*Scenario:* You are in charge of `storage`. For your next project milestone, your team plans to implement a new feature of saving the address book to the cloud. However, the current implementation of the application constantly saves the address book after the execution of each command, which is not ideal if the user is working on limited internet connection. Your team decided that the application should instead save the changes to a temporary local backup file first, and only upload to the cloud after the user closes the application. Your job is to implement a backup API for the address book storage. +*Scenario:* You are in charge of `storage`. For your next project milestone, your team plans to implement a new feature of saving the ePiggy to the cloud. However, the current implementation of the application constantly saves the ePiggy after the execution of each command, which is not ideal if the user is working on limited internet connection. Your team decided that the application should instead save the changes to a temporary local backup file first, and only upload to the cloud after the user closes the application. Your job is to implement a backup API for the ePiggy storage. [TIP] Do take a look at <> before attempting to modify the `Storage` component. -. Add a new method `backupAddressBook(ReadOnlyAddressBook)`, so that the address book can be saved in a fixed temporary location. +. Add a new method `backupEPiggy(ReadOnlyEPiggy)`, so that the ePiggy can be saved in a fixed temporary location. + **** * Hint -** Add the API method in link:{repoURL}/src/main/java/seedu/address/storage/AddressBookStorage.java[`AddressBookStorage`] interface. -** Implement the logic in link:{repoURL}/src/main/java/seedu/address/storage/StorageManager.java[`StorageManager`] and link:{repoURL}/src/main/java/seedu/address/storage/JsonAddressBookStorage.java[`JsonAddressBookStorage`] class. +** Add the API method in link:{repoURL}/src/main/java/seedu/address/storage/EPiggyStorage.java[`EPiggyStorage`] interface. +** Implement the logic in link:{repoURL}/src/main/java/seedu/address/storage/StorageManager.java[`StorageManager`] and link:{repoURL}/src/main/java/seedu/address/storage/JsonEPiggyStorage.java[`JsonEPiggyStorage`] class. * Solution ** See this https://github.com/se-edu/addressbook-level4/pull/594[PR] for the full solution. **** @@ -687,15 +1349,15 @@ By creating this command, you will get a chance to learn how to implement a feat *Scenario:* You are a software maintainer for `addressbook`, as the former developer team has moved on to new projects. The current users of your application have a list of new feature requests that they hope the software will eventually have. The most popular request is to allow adding additional comments/notes about a particular contact, by providing a flexible `remark` field for each contact, rather than relying on tags alone. After designing the specification for the `remark` command, you are convinced that this feature is worth implementing. Your job is to implement the `remark` command. ==== Description -Edits the remark for a person specified in the `INDEX`. + +Edits the remark for a expense specified in the `INDEX`. + Format: `remark INDEX r/[REMARK]` Examples: * `remark 1 r/Likes to drink coffee.` + -Edits the remark for the first person to `Likes to drink coffee.` +Edits the remark for the first expense to `Likes to drink coffee.` * `remark 1 r/` + -Removes the remark for the first person. +Removes the remark for the first expense. ==== Step-by-step Instructions @@ -705,12 +1367,12 @@ Let's start by teaching the application how to parse a `remark` command. We will **Main:** . Add a `RemarkCommand` that extends link:{repoURL}/src/main/java/seedu/address/logic/commands/Command.java[`Command`]. Upon execution, it should just throw an `Exception`. -. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to accept a `RemarkCommand`. +. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/EPiggyParser.java[`EPiggyParser`] to accept a `RemarkCommand`. **Tests:** . Add `RemarkCommandTest` that tests that `execute()` throws an Exception. -. Add new test method to link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`], which tests that typing "remark" returns an instance of `RemarkCommand`. +. Add new test method to link:{repoURL}/src/test/java/seedu/address/logic/parser/EPiggyParserTest.java[`EPiggyParserTest`], which tests that typing "remark" returns an instance of `RemarkCommand`. ===== [Step 2] Logic: Teach the app to accept 'remark' arguments Let's teach the application to parse arguments that our `remark` command will accept. E.g. `1 r/Likes to drink coffee.` @@ -719,17 +1381,17 @@ Let's teach the application to parse arguments that our `remark` command will ac . Modify `RemarkCommand` to take in an `Index` and `String` and print those two parameters as the error message. . Add `RemarkCommandParser` that knows how to parse two arguments, one index and one with prefix 'r/'. -. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to use the newly implemented `RemarkCommandParser`. +. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/EPiggyParser.java[`EPiggyParser`] to use the newly implemented `RemarkCommandParser`. **Tests:** . Modify `RemarkCommandTest` to test the `RemarkCommand#equals()` method. . Add `RemarkCommandParserTest` that tests different boundary values for `RemarkCommandParser`. -. Modify link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`] to test that the correct command is generated according to the user input. +. Modify link:{repoURL}/src/test/java/seedu/address/logic/parser/EPiggyParserTest.java[`EPiggyParserTest`] to test that the correct command is generated according to the user input. ===== [Step 3] Ui: Add a placeholder for remark in `PersonCard` -Let's add a placeholder on all our link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] s to display a remark for each person later. +Let's add a placeholder on all our link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] s to display a remark for each expense later. **Main:** @@ -741,28 +1403,28 @@ Let's add a placeholder on all our link:{repoURL}/src/main/java/seedu/address/ui . Modify link:{repoURL}/src/test/java/guitests/guihandles/PersonCardHandle.java[`PersonCardHandle`] so that future tests can read the contents of the remark label. ===== [Step 4] Model: Add `Remark` class -We have to properly encapsulate the remark in our link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] class. Instead of just using a `String`, let's follow the conventional class structure that the codebase already uses by adding a `Remark` class. +We have to properly encapsulate the remark in our link:{repoURL}/src/main/java/seedu/address/model/expense/Expense.java[`Expense`] class. Instead of just using a `String`, let's follow the conventional class structure that the codebase already uses by adding a `Remark` class. **Main:** -. Add `Remark` to model component (you can copy from link:{repoURL}/src/main/java/seedu/address/model/person/Address.java[`Address`], remove the regex and change the names accordingly). +. Add `Remark` to model component (you can copy from link:{repoURL}/src/main/java/seedu/address/model/expense/Address.java[`Address`], remove the regex and change the names accordingly). . Modify `RemarkCommand` to now take in a `Remark` instead of a `String`. **Tests:** . Add test for `Remark`, to test the `Remark#equals()` method. -===== [Step 5] Model: Modify `Person` to support a `Remark` field -Now we have the `Remark` class, we need to actually use it inside link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. +===== [Step 5] Model: Modify `Expense` to support a `Remark` field +Now we have the `Remark` class, we need to actually use it inside link:{repoURL}/src/main/java/seedu/address/model/expense/Expense.java[`Expense`]. **Main:** -. Add `getRemark()` in link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. -. You may assume that the user will not be able to use the `add` and `edit` commands to modify the remarks field (i.e. the person will be created without a remark). +. Add `getRemark()` in link:{repoURL}/src/main/java/seedu/address/model/expense/Expense.java[`Expense`]. +. You may assume that the user will not be able to use the `add` and `edit` commands to modify the remarks field (i.e. the expense will be created without a remark). . Modify link:{repoURL}/src/main/java/seedu/address/model/util/SampleDataUtil.java/[`SampleDataUtil`] to add remarks for the sample data (delete your `data/addressbook.json` so that the application will load the sample data when you launch it.) ===== [Step 6] Storage: Add `Remark` field to `JsonAdaptedPerson` class -We now have `Remark` s for `Person` s, but they will be gone when we exit the application. Let's modify link:{repoURL}/src/main/java/seedu/address/storage/JsonAdaptedPerson.java[`JsonAdaptedPerson`] to include a `Remark` field so that it will be saved. +We now have `Remark` s for `Expense` s, but they will be gone when we exit the application. Let's modify link:{repoURL}/src/main/java/seedu/address/storage/JsonAdaptedPerson.java[`JsonAdaptedPerson`] to include a `Remark` field so that it will be saved. **Main:** @@ -770,22 +1432,22 @@ We now have `Remark` s for `Person` s, but they will be gone when we exit the ap **Tests:** -. Fix `invalidAndValidPersonAddressBook.json`, `typicalPersonsAddressBook.json`, `validAddressBook.json` etc., such that the JSON tests will not fail due to a missing `remark` field. +. Fix `invalidAndValidPersonEPiggy.json`, `typicalPersonsEPiggy.json`, `validEPiggy.json` etc., such that the JSON tests will not fail due to a missing `remark` field. ===== [Step 6b] Test: Add withRemark() for `PersonBuilder` -Since `Person` can now have a `Remark`, we should add a helper method to link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`], so that users are able to create remarks when building a link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. +Since `Expense` can now have a `Remark`, we should add a helper method to link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`], so that users are able to create remarks when building a link:{repoURL}/src/main/java/seedu/address/model/expense/Expense.java[`Expense`]. **Tests:** -. Add a new method `withRemark()` for link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`]. This method will create a new `Remark` for the person that it is currently building. -. Try and use the method on any sample `Person` in link:{repoURL}/src/test/java/seedu/address/testutil/TypicalPersons.java[`TypicalPersons`]. +. Add a new method `withRemark()` for link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`]. This method will create a new `Remark` for the expense that it is currently building. +. Try and use the method on any sample `Expense` in link:{repoURL}/src/test/java/seedu/address/testutil/TypicalPersons.java[`TypicalPersons`]. ===== [Step 7] Ui: Connect `Remark` field to `PersonCard` Our remark label in link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] is still a placeholder. Let's bring it to life by binding it with the actual `remark` field. **Main:** -. Modify link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`]'s constructor to bind the `Remark` field to the `Person` 's remark. +. Modify link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`]'s constructor to bind the `Remark` field to the `Expense` 's remark. **Tests:** @@ -796,7 +1458,7 @@ We now have everything set up... but we still can't modify the remarks. Let's fi **Main:** -. Replace the logic in `RemarkCommand#execute()` (that currently just throws an `Exception`), with the actual logic to modify the remarks of a person. +. Replace the logic in `RemarkCommand#execute()` (that currently just throws an `Exception`), with the actual logic to modify the remarks of a expense. **Tests:** @@ -811,13 +1473,13 @@ See this https://github.com/se-edu/addressbook-level4/pull/599[PR] for the step- *Target user profile*: -* has a need to manage a significant number of contacts +* has a need to manage their expenses * prefer desktop apps over other types * can type fast * prefers typing over mouse input * is reasonably comfortable using CLI apps -*Value proposition*: manage contacts faster than a typical mouse/GUI driven app +*Value proposition*: manage expenses faster than a typical mouse/GUI driven app [appendix] == User Stories @@ -825,37 +1487,83 @@ See this https://github.com/se-edu/addressbook-level4/pull/599[PR] for the step- Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*` [width="59%",cols="22%,<23%,<25%,<30%",options="header",] -|======================================================================= +|============================================================ |Priority |As a ... |I want to ... |So that I can... -|`* * *` |new user |see usage instructions |refer to instructions when I forget how to use the App +|`* * *` |user |add a new expense record |track my expenses -|`* * *` |user |add a new person | +|`* * *` |user |delete expense records |remove wrongly entered expense records -|`* * *` |user |delete a person |remove entries that I no longer need +|`* * *` |user |add my allowances received |know my total savings -|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +|`* * *` |new user |see usage instructions |refer to a full list of instructions when I forget how to use ePiggy -|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +|`* * *` |user |view my total expenditure within specified dates or by time period |know the total amount I have spent with a certain time period -|`*` |user with many persons in the address book |sort persons by name |locate a person easily -|======================================================================= +|`* *` |user |see the report of my spending on a specified year|analyse and be aware of my spending habits for that year -_{More to be added}_ +|`* *` |user |see the report of my spending on specified date |know how much I spend on certain days + +|`* *` |user |see the report of my spending on specified month of the year |know how much I spend on a certain month of a year + +|`* *` |user |see the statistics of my spendings till date |see my spending habits + +|`* *` |user |see the date of my spending |know when I spend a certain item + +|`* *` |user |set a budget based on a time period |know whether I am keeping to the budget + +|`* *` |user |see how much more I can spend out of my set budget |be aware of how much I am spending + +|`* *` |user |receive reminders when I am approaching my budget |cut down on my expenses for the remaining time period + +|`* *` |user |get reminders when I have exceeded my budget |aware that I have not kept to my set budget + +|`* *` |user |set a savings goal |be more motivated to save up enough money to buy the item specified in the goal + +|`* *` |user |know how much more I have to save before I reach my savings goal |know I am making progress in saving up to reach the goal + +|`* *` |user |type a command with auto-complete |speed up the typing + +|`*` |user |login to my personal account |manage my personal expense records + +|`*` |new user |create my personal account |make my personal expense records confidential + +|============================================================ [appendix] == Use Cases -(For all use cases below, the *System* is the `AddressBook` and the *Actor* is the `user`, unless specified otherwise) +(For all use cases below, the *System* is the `ePiggy` and the *Actor* is the `user`, unless specified otherwise) + +[discrete] +=== Use case: Add expense record + +*MSS* + +1. User requests to create a new record. +2. User enters an add command with the name of item, cost, categories and date. +3. ePiggy saves the record. ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The name and date are empty. ++ +[none] +** 2a1. ePiggy shows an error message. ++ +Use case resumes at step 2. [discrete] -=== Use case: Delete person +=== Use case: Edit expense records *MSS* -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User requests to list expenses +2. ePiggy shows a list of expenses +3. User requests to edit a specific expense in the list +4. ePiggy edits the expense + Use case ends. @@ -869,17 +1577,379 @@ Use case ends. * 3a. The given index is invalid. + [none] -** 3a1. AddressBook shows an error message. +** 3a1. ePiggy shows an error message. + Use case resumes at step 2. +[discrete] +=== Use case: Delete expense records + +*MSS* + +1. User requests to delete expense record. +2. ePiggy requests user to enter delete command. +3. User enters delete command with index of the expense record. +4. ePiggy perform delete action ++ +Use case ends. + +*Extensions* + +[none] +* 1a. The period of expenses is less than a week. ++ +[none] +** 1a1. ePiggy shows an error message. ++ +Use case ends. + +// tag::allowanceusecases[] +[discrete] +=== Use case: Add allowance records +*MSS* + +1. User request to create a new allowance record. +2. User enters command with the item name, cost, tags and date. +3. ePiggy saves the record. ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The name or cost fields are empty ++ +[none] +** 2a1. ePiggy shows an error message. ++ +Use case ends. + +[none] +* 2b. The name, cost, tag or date are invalid. ++ +[none] +** 2b1. ePiggy shows an error message. ++ +Use case ends. + +[discrete] +=== Use case: Edit allowance records +*MSS* + +1. User request to list expenses and allowances. +2. ePiggy shows the list. +3. User requests to edit a specific allowance in the list. +4. User enters edit allowance command with index of the allowance record. +5. ePiggy edits the record. ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The list is empty ++ +Use case ends. + +[none] +* 4a. The given index is invalid. ++ +[none] +** 4a1. ePiggy shows an error message. ++ +Use case resumes at step 2. + +[none] +* 4b. The given index points to an allowance. ++ +[none] +** 4b1. ePiggy shows an error message. ++ +Use case resumes at step 2. + +[discrete] +=== Use case: Delete allowance records + +*MSS* + +1. User request to list expenses and allowances. +2. ePiggy shows the list. +3. User requests to delete an allowance record. +4. User enters delete allowance command with index of the allowance record. +5. ePiggy deletes the specified allowance. ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The list is empty. ++ +Use case ends. + +[none] +* 4a. The given index is invalid. ++ +[none] +** 4a1. ePiggy shows an error message. ++ +Use case resumes at step 2. + +[none] +* 4b. The given index points to an allowance. ++ +[none] +** 4b1. ePiggy shows an error message. ++ +Use case resumes at step 2. +// end::allowanceusecases[] + +[discrete] +=== Use case: Search expense records + +*MSS* + +1. User requests to search for an expense record. +2. ePiggy requests user to enter search command. +3. User enters search command with specific parameters. +4. ePiggy searches and displays the record(s). ++ +Use case ends. + +*Extensions* + +[none] +* 1a. Parameter field is empty. ++ +[none] +** 1a1. ePiggy lists all the expense records. ++ +Use case ends. + +[discrete] +=== Use case: Sort expense records + +*MSS* + +1. User requests to sort expense records. +2. ePiggy requests user to enter sort command. +3. User enters sort command with specific parameters. +4. ePiggy sorts and displays the record(s). ++ +Use case ends. + +*Extensions* + +[none] +* 1a. Parameter field is empty. ++ +[none] +** 1a1. ePiggy lists all the expense records sorted by date added. ++ +Use case ends. + + +[discrete] +=== Use case: Add budget + +*MSS* + +1. User requests to add budget. +2. User enters the `addBudget` command with the budgeted amount, time period and start date of budget. +3. ePiggy saves the record. ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The given amount, time period and/or start date are empty or invalid. ++ +[none] +** 2a1. ePiggy shows an error message. ++ +Use case resumes at step 1. + +* 2b. User already has a budget set which overlaps with the new budget. ++ +[none] +** 2b1. ePiggy shows can error message ++ +Use case resumes at step 1. + +[discrete] +=== Use case: Edit budget + +*MSS* + +1. User requests to edit budget. +2. User enters the `editBudget` command with the budgeted amount, time period and/or start date of budget. +3. ePiggy saves the record. ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The given amount, time period and/or start date are invalid or all are empty. ++ +[none] +** 2a1. ePiggy shows an error message. ++ +Use case resumes at step 1. + +* 2b. User already has a budget set which overlaps with the edited budget. ++ +[none] +** 2b1. ePiggy shows can error message ++ +Use case resumes at step 1. + +[discrete] +=== Use case: Delete budget + +*MSS* + +1. User requests to delete a budget. +2. User enters the `deleteBudget` command with the index of the budget. +3. ePiggy performs the delete action. ++ +Use case ends. + +*Extensions* + +[none] +* 1a. Index of budget does not exist in the budget list. ++ +[none] +** 1a1. ePiggy shows an error message. ++ +Use case resumes at step 1. + +[discrete] +=== Use case: Display a reminder when a budget is finishing + +*MSS* + +1. User budget is finishing soon +2. ePiggy's reminder message turns to a darker colour and warns the user that their budget is finishing. ++ +Use case ends. + +[discrete] +=== Use case: Auto-complete command when user press Tab key + +*MSS* + +1. User types some letters in the commandBox +2. User presses Tab key +3. ePiggy auto-complete the command according to the letters which user typed in the commandBox + ++ +Use case ends. + _{More to be added}_ +[discrete] +=== Use case: Display a reminder when a budget has exceeded + +*MSS* + +1. User budget is finishing soon +2. ePiggy's reminder message turns red and the alerts the user that budget has exceeded. ++ +Use case ends. + +// tag::goalusecases[] +[discrete] +=== Use case: Set a savings goal + +*MSS* + +1. User request to set a savings goal. +2. User sets a savings goal with the goal name and amount. +3. ePiggy saves the goal. ++ +Use case ends. + +*Extensions* + +[none] +* 2a. Name or amount specified are invalid. ++ +[none] +** 2a1. ePiggy shows an error message. ++ +Use case ends. +// end::goalusecases[] + +[discrete] +=== Use case: View report + +*MSS* + +1. User requests to view expenditure report of a day/month/year. +2. ePiggy shows the expenditure report for that day/month/year. ++ +Use case ends. + +*Extensions* + +[none] +* 1a. The given date is invalid. ++ +[none] +** 1a1. ePiggy shows an error message. ++ +Use case ends. + +[discrete] +=== Use case: Login + +*MSS* + +1. ePiggy requests username and password +2. User enters username and password +3. User login success. ++ +Use case ends. + +*Extensions* + +[none] +* 1a. Username and password do not match. ++ +[none] +** 1a1. ePiggy shows an error message. +** 1a2. ePiggy recovers from 1. ++ +Use case ends. + +[discrete] +=== Use case: Help + +*MSS* + +1. User requests for help. +2. ePiggy displays a list of all the commands - brief description and syntax. ++ +Use case ends. + +[discrete] +=== Use case: History + +*MSS* + +1. User requests to list all the entered commands. +2. ePiggy shows all the commands entered (except empty commands) in reverse. ++ +Use case ends. + [appendix] == Non Functional Requirements . Should work on any <> as long as it has Java `9` or higher installed. -. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +. Should be able to hold up to 1000 expenses without a noticeable sluggishness in performance for typical usage. . 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. _{More to be added}_ @@ -890,26 +1960,6 @@ _{More to be added}_ [[mainstream-os]] Mainstream OS:: Windows, Linux, Unix, OS-X -[[private-contact-detail]] Private contact detail:: -A contact detail that is not meant to be shared with others - -[appendix] -== Product Survey - -*Product Name* - -Author: ... - -Pros: - -* ... -* ... - -Cons: - -* ... -* ... - [appendix] == Instructions for Manual Testing @@ -932,26 +1982,258 @@ These instructions only provide a starting point for testers to work on; testers .. Re-launch the app by double-clicking the jar file. + Expected: The most recent window size and location is retained. -_{ more test cases ... }_ +// tag::manualtestingallowance[] +=== Adding an allowance + +. Adding an allowance to the list of allowances and expenses. + +.. Test case: `addAllowance n/From Mom $/50 t/School d/04/02/2019` + + Expected: Allowance added to the list. Details of allowance shown in the command box + as well as on the list. An `allowance` tag is automatically added to the entry. +.. Test case: `addAllowance $/200` + + Expected: Allowance not added to the list. Error details shown in the command box. +.. Other incorrect `addAllowance` commands to try: `addAllowance`, `addAllowance n/NameOnly`, +`addAllowance n/From Dad $/30 t/Expense`, `addAllowance n/From Brother $/0` + +=== Editing an allowance + +. Editing an allowance while there are allowances in the list of allowances and expenses. +.. Prerequisite: There is an allowance record in the first index of the list. +.. Test case: `editAllowance 1 $/20` + + Expected: Allowance at index 1 updated to be $20. Details of the updated allowance is shown + in the command box and on the list. +.. Test case: `editAllowance 2 $/40` + + Expected: Assuming the entry at index 2 is an expense, entry at index 2 is not edited. + Error message is shown in the command box. +.. Other incorrect `editAllowance` commands to try: `editAllowance $/40`, `editAllowance 1 t/Expense`, + `editAllowance 1 $/0` +// end::manualtestingallowance[] + +=== Deleting an allowance +. Deleting an allowance while there are allowances in the list of allowances and expenses. +.. Prerequisite: There is an allowance record in the first index of the list. +.. Test case: `deleteAllowance 1` + + Expected: Allowance at index 1 is deleted. Details of the deleted allowance is shown in the + command box, and removed from the list. +.. Test case: `deleteAllowance 2` + + Expected: Assuming the entry at index 2 is an expense, it is not deleted. Details to the + error message is shown in the command box. +.. Other incorrect `deleteAllowance` commands to try: `deleteAllowance`, `deleteAllowance b`, + `deleteAllowance 99999` (assuming the list is shorter than 99999) + + +=== Deleting an expense + +. Deleting an expense while all expenses are listed + +.. Prerequisites: List all expenses using the `list` command. Multiple expenses in the list. +.. Test case: `deleteExpense 1` + + Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. +.. Test case: `deleteExpense 0` + + Expected: No expense is deleted. Error details shown in the status message. Status bar remains the same. +.. Other incorrect deleteExpense commands to try: `deleteExpense`, `deleteExpense x` (where x is larger than the list size) _{give more}_ + + Expected: Similar to previous. -=== Deleting a person +=== Adding a budget -. Deleting a person while all persons are listed +. Adding a budget while there are no budgets -.. Prerequisites: List all persons using the `list` command. Multiple persons in the list. -.. Test case: `delete 1` + - Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. -.. Test case: `delete 0` + - Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. -.. Other incorrect delete commands to try: `delete`, `delete x` (where x is larger than the list size) _{give more}_ + +.. Prerequisites: Make sure the budget list is empty. +.. Test case: `addBudget $/500 p/28 d/01/02/2019` + + Expected: Budget is added to the list. Details of the added budget is shown. Budget added can be seen in the budget list. +.. Test case: `addBudget $/500 p/28` + + Expected: Budget is not added. Error details shown in the status message. +.. Other incorrect addBudget commands to try: `addBudget`, `addBudget $/ p/ d/ `, `addBudget p/1 d/01/02/2019` + Expected: Similar to previous. -_{ more test cases ... }_ +. Adding a budget while there are existing budgets + +.. Prerequisites: Multiple budgets in the budget list. +.. Test case: `addBudget $/100 p/1 d/01/02/2019` (does not overlap with any existing budgets) + + Expected: Budget is added to the list. Details of the added budget is shown. Budget added can be seen in the budget list. +.. Test case: `addBudget $/10000 p/365 d/01/01/2019` + + Expected: Budget is not added. Error details shown in the status message (overlapping budgets). -=== Saving data +=== Editing a budget -. Dealing with missing/corrupted data files +. Editing a budget while there is an existing current budget and no other budgets -.. _{explain how to simulate a missing/corrupted file and the expected behavior}_ +.. Prerequisites: No other budgets in the budget list besides a current budget. +.. Test case: `editBudget $/100` + + Expected: Current budget is updated to be $100. Details of the edited budget is shown. Edited budget can be seen in the budget list. +.. Test case: `editBudget` + + Expected: Budget is not edited. Error details shown in the status message. +.. Other incorrect addBudget commands to try: `editBudget $/ `, `editBudget $/ p/ d/ ` + + Expected: Similar to previous. -_{ more test cases ... }_ +. Editing a budget while there is no current budget + +.. Prerequisites: Make sure there are no current budgets present in the budget list. +.. Test case: `editBudget $/500` + + Expected: Budget is not edited. Error details shown in the status message. +.. Other incorrect addBudget commands to try: `addBudget`, `addBudget $/ p/ d/ `, `addBudget p/1 d/01/02/2019` + + Expected: Similar to previous. + +=== Deleting a budget + +. Deleting a budget from the budget list + +.. Prerequisites: List all expenses using the `list` command. Multiple expenses in the list. +.. Test case: `deleteBudget 1` + + Expected: First budget is deleted from the list. Details of the deleted budget shown. +.. Test case: `deleteBudget 0` + + Expected: No budget is deleted. Error details shown. +.. Other incorrect deleteExpense commands to try: `deleteBudget`, `deleteBudget x` (where x is larger than the list size) + + Expected: Similar to previous. + +// tag::manualtestinggoal[] +=== Setting a savings goal +. Setting a savings goal to ePiggy. +.. Test case: `setGoal n/iPad $/599` + Expected: Goal successfully set. Details of the goal is found in the command box and on the + user interface. +.. Test case: `setGoal $/999` + Expected: Goal is not set. Error message shown in the command box. +.. Other incorrect `setGoal` commands to try: `setGoal`, `setGoal n/@pple watch $/499`, + `setGoal n/No Goal $/0` +// end::manualtestinggoal[] + +=== Finding an expense + +[NOTE] +Please input the following commands below (in order). + +1. `clear` + +2. `ae n/KFC $/4.5 d/04/05/2019 t/food` + +3. `ae n/Chicken rice $/6 d/08/05/2019 t/food t/lunch` + +4. `ae n/Movie $/13 d/10/05/2019 t/movie t/friends` + +5. `ae n/KFC $/4.5 d/10/05/2019 t/food t/friends` + + +Prerequisites: Make sure the expense list is NOT empty and you have typed in the commands +above. + +. Find by `name` + +.. Test case: `find n/kfc` + + Expected: 2 expenses will be shown in the expense list panel. +.. Test case: `find n/rice` + + Expected: 1 expense will be shown. +.. Test case: `find n/movei` + + Expected: 1 expense will be shown. +.. Test case: `find n/mee goreng` + + Expected: 0 expenses will be shown. +.. Test case: `find n/` + + Expected: invalid command error. + +. Find by `cost` + +.. Test case: `find $/1:10` + + Expected: 3 expenses will be shown in the expense list panel. +.. Test case: `find $/6` + + Expected: 1 expense will be shown. +.. Test case: `find $/1` + + Expected: 0 expenses will be shown. +.. Test case: `find $/10:1` + + Expected: invalid cost error. +.. Test case: `find $/` + + Expected: invalid cost error. + +. Find by `date` + +.. Test case: `find d/10/05/2019` + + Expected: 2 expenses will be shown in the expense list panel. +.. Test case: `find d/04/05/2019:09/05/2019` + + Expected: 2 expenses will be shown. +.. Test case: `find d/04/03/2019` + + Expected: 0 expenses will be shown. +.. Test case: `find d/2019/05/04` + + Expected: invalid date error. +.. Test case: `find d/10/05/2019:04/05/2019` + + Expected: invalid date error. +.. Test case: `find d/` + + Expected: invalid date error. + + +. Find by `tag` + +.. Test case: `find t/friends` + + Expected: 2 expenses will be shown in the expense list panel. +.. Test case: `find t/freinds` + + Expected: same as previous. +.. Test case: `find t/friends t/food` + + Expected: 1 expense will be shown. +.. Test case: `find t/` + + Expected: invalid tag error. + +. Find using multiple keywords + +.. Test case: `find $/1:10 d/10/05/2019` + + Expected: 1 expense will be shown in the expense list panel. +.. Test case: `find n/kfc t/friends` + + Expected: 1 expense1 will be shown. +.. Test case: `find n/kfc d/05/05/2019:08/05/2019` + + Expected: 0 expenses will be shown. + +=== Sorting expenses + +[NOTE] +Please input the following commands below (in order). + +1. `clear` + +2. `ae n/KFC $/4.5 d/04/05/2019 t/food` + +3. `ae n/Chicken rice $/6 d/08/05/2019 t/food t/lunch` + +4. `ae n/Movie $/13 d/10/05/2019 t/movie t/friends` + +5. `ae n/KFC $/4.5 d/10/05/2019 t/food t/friends` + + +Prerequisites: Make sure the expense list is NOT empty and you have typed in the commands +above. + +. Sort by `name` + +.. Test case: `sort n/` + + Expected: all expenses ordered in ascending order (of name) will be shown in the expense list panel. +.. Test case: `sort n/kfc` + + Expected: invalid command error. + +. Sort by `cost` + +.. Test case: `sort $/` + + Expected: all expenses ordered in descending order (of cost) will be shown in the expense list panel. +.. Test case: `sort $/` and then 'rl' + + Expected: all expenses ordered in ascending order (of cost) will be shown in the expense list panel. +.. Test case: `sort $/10` + + Expected: invalid command error. + +. Sort by `date` + +.. Test case: `sort d/` + + Expected: all expenses ordered in descending order (of date) will be shown in the expense list panel. +.. Test case: `sort d/` and then 'rl' + + Expected: all expenses ordered in ascending order (of date) will be shown in the expense list panel. +.. Test case: `sort d/10/05/2019` + + Expected: invalid command error. + +. Sort using multiple keywords + +.. Test case: `find $/ d/` + + Expected: Invalid command error. + +=== Generating a report + +. Generating a report on specified date + +.. Prerequisites: There are some expense or allowance records in ePiggy. +.. Test case: `report` + + Expected: New window pop-up. Details of the expense, allowance and budget list in the new window. + Success message shows in the result display textBox. +.. Test case: `report d/21/03/2019` + + Expected: New window pop-up. Details of the expense, allowance and budget list in the new window. + Success message shows in the result display textBox. +.. Test case: `report d/invalid text` + + Expected: New window doesn't pop-up. + Error message shows in the result display textBox. +.. Test case: `report d/1000/03/2019` + + Expected: New window doesn't pop-up. + Error message shows in the result display textBox. +.. Other incorrect report commands to try: `report invalid`, `report d/x` (where x is not in dd/mm/yyyy or mm/yyyy or yyyy format), + `report d/x` (where x is not a correct date) _{give more}_ + + + Expected: Similar to previous. diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 7e0070e12f49..fc2857117871 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -1,6 +1,7 @@ -= AddressBook Level 4 - User Guide += ePiggy - User Guide :site-section: UserGuide :toc: +:toclevels: 4 :toc-title: :toc-placement: preamble :sectnums: @@ -12,249 +13,616 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4 +:repoURL: https://github.com/CS2103-AY1819S2-W17-4/main -By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` +image::userguide.png[width="700"] -== Introduction +By: `Team A+ for CS2103T` Since: `Feb 2019` Last Updated: `April 2019` Licence: `MIT` -AddressBook Level 4 (AB4) is for those who *prefer to use a desktop app for managing contacts*. More importantly, AB4 is *optimized for those who prefer to work with a Command Line Interface* (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB4 can get your contact management tasks done faster than traditional GUI apps. Interested? Jump to the <> to get started. Enjoy! +== Introduction -== Quick Start +image::userguideintroduction.gif[width="200", role="center"] -. Ensure you have Java version `9` or later installed in your Computer. -. Download the latest `addressbook.jar` link:{repoURL}/releases[here]. -. Copy the file to the folder you want to use as the home folder for your Address Book. -. Double-click the file to start the app. The GUI should appear in a few seconds. -+ -image::Ui.png[width="790"] -+ -. Type the command in the command box and press kbd:[Enter] to execute it. + -e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. -. Some example commands you can try: +Welcome to *_ePiggy_*! *_ePiggy_* is a desktop application designed to inculcate good spending habits in students. + +Managing money is made much simpler with a simple interface and simple commands! +At the same time, *_ePiggy_* offers everything you need to cultivate good spending habits before +entering the workforce, from tracking expenses and managing budgets to setting goals! + -* *`list`* : lists all contacts -* **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : adds a contact named `John Doe` to the Address Book. -* **`delete`**`3` : deletes the 3rd contact shown in the current list -* *`exit`* : exits the app +Interested? Click on <> to get started! -. Refer to <> for details of each command. +== Quick Start +To start using ePiggy, simply follow these steps::: +1. Ensure you have Java version `9` or later installed on your computer. + +2. Download the latest *`ePiggy.jar`* file link:{repoURL}/releases[here]. + +3. Copy the file to the folder you wish to use as the home folder for ePiggy. + +4. Double-click the *`ePiggy.jar`* file to start the app. + +5. The GUI will appear with some sample data upon launch, as per below. + +image::Firstlaunch.png[width="760"] +_Figure 1. The user interface upon launch of application_ + +image::samplecommands.png[width="175"] + +* `addExpense n/Chicken Rice $/4 t/dinner d/14/04/2019` + +This command adds a new expense of $4 for Chicken Rice on 14th April. +* `addAllowance n/Monthly Allowance $/600 d/01/04/2019` + +This command adds a new allowance of $600 tagged as 'Monthly Allowance' on 1st April. +* `addBudget $/500 p/30 d/01/04/2019` + +This command adds a budget of $500 from 1st April 2019 to 30th April 2019 (30 days). +* `setGoal n/Apple Watch $/600` + +This command sets a goal of $600 for an Apple Watch. +* `sort d/` + +This command sorts the expenses and allowance list by date. + +image::Ui.png[width="760"] +_Figure 2. A sample of the user interface with the above commands entered, together with other commands._ [[Features]] == Features ==== -*Command Format* - -* Words in `UPPER_CASE` are the parameters to be supplied by the user e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. -* Items in square brackets are optional e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. -* Items with `…`​ after them can be used multiple times including zero times e.g. `[t/TAG]...` can be used as `{nbsp}` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. -* Parameters can be in any order e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. +image::commandformat.png[width="175"] + +* Words in `UPPER_CASE` are the parameters to be entered by the user. + +** E.g. For `addExpense n/EXPENSE_NAME`, `EXPENSE_NAME` is a parameter which can be used as `addExpense n/Chicken Rice`. +* Items in square brackets are optional. + +** E.g `n/EXPENSE_NAME [t/TAG]` can be used as `n/Chicken Rice t/School` or as `n/Chicken Rice`. +* Items with `…`​ after them can be used multiple times including zero times. + +** E.g. `[t/TAG]...` can be used as `{nbsp}` (i.e. 0 times), `t/school` (1 time), `t/hawker t/school` (2 times) etc. +* Parameters can be in any order. + +** E.g. If the command specifies `n/EXPENSE_NAME $/COST`, `$/COST n/EXPENSE_NAME` is also acceptable. ==== -=== Viewing help : `help` +image::callouts.png[width="175"] -Format: `help` +Callouts are boxes with icons to point out some information. These are the 2 callouts used throughout this user guide: -=== Adding a person: `add` - -Adds a person to the address book + -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` +[NOTE] +This represents a *note*. A note indicates important, additional information. Be sure to read these notes as they might be applicable to you! [TIP] -A person can have any number of tags (including 0) +This represents a *tip*. A tip denotes something that is often handy, and good for you to know. Tips are often less crucial, and you can choose to skip them. + + +=== Basic Features +// tag::autocomplete[] +==== Using auto-complete : tab +Automatically completes your input without requiring you to type them in full. + +To use this feature, enter the first few letters of your intended command and press `Tab`. -Examples: +[NOTE] +If the completed command is not your intended command, you can delete the command, enter the same letters again and press `Tab`. +Another command will show if their letters match. + + +image::examples.png[width="125"] +* *Enter letter 'a', then press `Tab`* + +The command "addExpense n/ $/ t/ d/ " will show. + +* *Delete the command above, enter 'a' again and press `Tab`* + +The `addBudget` or `addAllowance` command will show. -* `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` +**** +image::additionalinformation.png[width="175"] +Entering 'as' and pressing `Tab` will cause *nothing* to happen because 'as' does not match any commands. +Auto-complete does *not support alias* and is *non-case sensitive*. +**** -=== Listing all persons : `list` +// end::autocomplete[] -Shows a list of all persons in the address book. + -Format: `list` +==== Viewing help : `help` +Lists all the user commands with their syntax and descriptions. + +*Alias:* `hp` + +*Format:* `help` -=== Editing a person : `edit` +// tag::aed[] +==== Adding an expense : `addExpense` -Edits an existing person in the address book. + -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` +Adds a new expense record to ePiggy. + +*Alias:* `ae` + +*Format:* `addExpense n/EXPENSE_NAME $/COST [d/DATE] [t/TAG]…` + + +image::examples.png[width="125"] + +* `addExpense n/Chicken rice set $/5 t/Food d/21/02/2019` + +Adds an expense for a $5 chicken rice set, tagged as Food with the date as 21/02/2019. + +* `addExpense n/Chicken rice set $/5 t/Food` + +Adds an expense for a $5 chicken rice set, tagged as Food dated as the current date, by default. **** -* 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. +image::additionalinformation.png[width="175"] +Creating an expense would automatically tag the entry with an `Expense` tag. + +`Name` has to be alphanumeric with a length of less than 50 characters. + +`AMOUNT` has to be a valid amount greater than $0, and less than $999,999.99. + +`Date` follows the format `dd/MM/yyyy`. **** -Examples: +==== Editing an expense : `editExpense` + +Edits an existing expense in ePiggy at a specific `*INDEX`* . + +The *`INDEX`* refers to the number in the displayed Expenses List which is next to the name of the expense. Existing values of +the expense will be changed according to the value of the parameters. + +*Alias:* `ee` + +*Format:* `editExpense INDEX [n/EXPENSE_NAME] [$/COST] [d/DATE] [t/TAG]…` + -* `edit 1 p/91234567 e/johndoe@example.com` + -Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` + -Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. -=== Locating persons by name: `find` +image::examples.png[width="125"] -Finds persons whose names contain any of the given keywords. + -Format: `find KEYWORD [MORE_KEYWORDS]` +* `editExpense 1 n/Pen $1 t/Supplies` + +Edits the name, cost and tag of the first expense in the Expense List to ‘Pen’, ‘$1’ and ‘Supplies’ respectively. +* `editExpense 2 t/Food` + +Edits the tag of the second expense in the Expense List to ‘Food’. + +==== Deleting an expense : `deleteExpense` + +Deletes the expense at the specified `*INDEX`*. + +The `*INDEX`* refers to the number in the displayed Expenses List which is next to +the name of the expense. + +*Alias:* `de` + +*Format:* `deleteExpense INDEX` + + +image::example.png[width="125"] + +* `deleteExpense 1` + +Deletes the first expense in the displaying expense list from ePiggy. +// end::aed[] + +// tag::aa[] +==== Adding an allowance: `addAllowance` + +Adds a new allowance record to ePiggy. + +*Alias:* `aa` + +*Format:* `addAllowance n/ALLOWANCE_NAME $/AMOUNT [d/DATE] [t/TAG]…` + +image::example.png[width="125"] + +* `addAllowance n/From Mom $/20 t/School d/21/02/2019` + +Adds an allowance of $20 from Mom, tagged as School with the date as 21/02/2019. **** -* The search is case insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +image::additionalinformation.png[width="175"] +Creating an allowance would automatically tag the entry with an `Allowance` tag. + +`Name` has to be alphanumeric with a length of less than 50 characters. + +`AMOUNT` has to be a valid amount greater than $0, and less than $999,999.99. + +`Date` follows the format `dd/MM/yyyy`. **** +// end::aa[] +// tag::ea[] +==== Editing an allowance : `editAllowance` + +Edits an existing allowance in ePiggy at a specific `*INDEX`* . + +The *`INDEX`* refers to the number next to the name of the allowance in the displayed allowances and expenses list. Existing values of +the allowance will be changed according to the value of the parameters. + +*Alias:* `ea` + +*Format:* `editAllowance INDEX [n/ALLOWANCE_NAME] [$/AMOUNT] [d/DATE] [t/TAG]…` -Examples: +image::examples.png[width="125"] -* `find John` + -Returns `john` and `John Doe` -* `find Betsy Tim John` + -Returns any person having names `Betsy`, `Tim`, or `John` +* `editAllowance 1 n/From Mom $10 t/Emergency` + +Edits the name, cost and tag of the first allowance in ePiggy to ‘From Mom’, ‘$10’ and ‘Emergency’ respectively. +* `editAllowance 2 $/22` + +Edits the amount of the second allowance in ePiggy to ‘$22’. -=== Deleting a person : `delete` +**** +image::additionalinformation.png[width="175"] +Only allowances can be edited using this command. To edit expenses, please use the `editExpense` command. +**** +// end::ea[] +// tag::da[] +==== Deleting an allowance : `deleteAllowance` + +Deletes the allowance at the specified `*INDEX`*. + +The `*INDEX`* refers to the number next to the name of the allowance in the displayed allowances and +expenses list. + +*Alias:* `da` + +*Format:* `deleteAllowance INDEX` -Deletes the specified person from the address book. + -Format: `delete INDEX` +image::example.png[width="125"] + +* `deleteExpense 1` + +Deletes the first allowance in the displaying list from ePiggy. **** -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index *must be a positive integer* 1, 2, 3, ... +image::additionalinformation.png[width="175"] +Only allowances can be deleted using this command. To delete expenses, please use the `deleteExpense` command. **** +// end::da[] + +==== Listing all expenses : `list` +// tag::list[] + +Lists the expense records from newest to oldest. Use this to return to the default view after search/sort commands. + + +*Alias:* `l` + +*Format:* `list` +// end::list[] + +==== Listing all expenses in reverse : `reverseList` +// tag::reverse[] + +Lists the expense records from oldest to newest. Use this to return to the default view after search/sort commands. + -Examples: +*Alias:* `rl` + +*Format:* `reverseList` -* `list` + -`delete 2` + -Deletes the 2nd person in the address book. -* `find Betsy` + -`delete 1` + -Deletes the 1st person in the results of the `find` command. +// end::reverse[] -=== Selecting a person : `select` +==== Listing entered commands : `history` -Selects the person identified by the index number used in the displayed person list. + -Format: `select INDEX` +// tag::history[] + +Lists all the user entered commands from newest to oldest. This command does not list empty commands. + +*Alias:* `hs` + +*Format:* `history` + +// end::history[] + +=== Filtering and Sorting Data + +// tag::fe[] +==== Finding an expense or allowance : `find` + +Finds any expense or allowance in the list by specifying either its name, tag, date, range of dates, amount or range of amount. + +*Alias:* `fd` + +*Format:* *`find [n/NAME] [t/TAG] [d/DATE_RANGE] [$/AMOUNT RANGE]`* + + +** Examples: + +* `find n/McDonalds` + +Displays all entries with the name “McDonalds”. + +* `find t/FOOD` + +Displays all entries with the tag specified (in this case, it’s food). + +* `find d/02/01/2019` + +Displays all entries listed on 2nd Jan 2019. + +* `find d/02/01/2019:05/12/2020` + +Displays all entries listed in the range 2nd Jan 2019 to 5th Dec 2020 (both inclusive). + +* `find $/250` + +Displays all entries listed with the cost range of $250. + +* `find $/250:500` + +Displays all entries listed with the cost range of $250 to $500. + + +**** +*Additional Information:* + +Searches and displays the entry along with its information, according to the user-specified command. + +Searching for names and tags is case-insensitive. Furthermore, it allows you to search for almost similar +words by applying the concept of Levenshtien distance, hence allowing small typos (limit fixed by an upper bound). + +If the entry is not found, it displays an appropriate error message. + +Date format is `dd/MM/yyyy` . +**** +// end::fe[] + +// tag::se[] +==== Sorting the expenses and allowances : `sort` + +Sorts the expenses and allowances in the list by name, date added, amount in ascending or descending order. + +*Alias:* `st` + +*Format:* `sort [n/d/$]/` + +** Examples: + +* `sort n/` + +Sorts all entries by name (in ascending order). + +* `sort d/` + +Sorts all entries by date in descending order. + +* `sort $/` + +Sorts all entries by amount in ascending order. + **** -* Selects the person and loads the Google search page the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index *must be a positive integer* `1, 2, 3, ...` + +*Additional Information:* + +There should be only one keyword (either `name`, `cost` or `date`) to determine the sorting sequence. + **** -Examples: +// end::se[] + +=== Budgeting -* `list` + -`select 2` + -Selects the 2nd person in the address book. -* `find Betsy` + -`select 1` + -Selects the 1st person in the results of the `find` command. +==== Adding a budget : `addBudget` -=== Listing entered commands : `history` +// tag::addbudget[] -Lists all the commands that you have entered in reverse chronological order. + -Format: `history` +Adds a budget for the total expenses within the specified time period. The time period will be in terms of days, +and 1 day is the minimum a person can set a budget for. + +Budgets added are not allowed to overlap with existing budgets. + [NOTE] -==== -Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. -==== +Budgets are considered to be overlapping if their active dates intersect each other. -// tag::undoredo[] -=== Undoing previous command : `undo` +*Alias:* `ab` + +*Format:* `addBudget $/AMOUNT p/TIME_PERIOD_IN_DAYS d/START_DATE` -Restores the address book to the state before the previous _undoable_ command was executed. + -Format: `undo` +image::examples.png[width="125"] + +* `addBudget $/500 p/7 d/03/02/2019` + +Sets a total budget of $500 for 7 days starting from 3rd February 2019. + +* `addBudget $/10000 p/15 d/01/01/2000` + +Sets a total budget of $10000 for 15 days starting from 1st January 2000. + +**** +image::additionalinformation.png[width="175"] +Time period cannot exceed 1 million days. + +Budgeted amount has to be a valid amount greater than $0, and less than $999,999.99. + +Date follows the format `dd/MM/yyyy`. + +Budget does not take into account allowances as budget only accounts for expenses. +**** +// end::addbudget[] + +*Display of budget:* + +ePiggy will show the status of the budgets whenever a new expense is added, edited or deleted. + + +image::budgetstatus.png[width="300"] +_Figure 3. Example of the status of a current budget._ + +There will also a reminder for you, depending on the status of your budget. There are 4 different reminders, as shown +below. + +image::budgetExceeded.png[width="300"] +_Figure 4. Reminder shown when you have exceeded your budget._ + +image::budgetEmpty.png[width="300"] +_Figure 5. Reminder shown when you have $0 left in your budget._ + +image::budget80percent.png[width="300"] +_Figure 6. Reminder shown when you have spent more than 80% of the budget._ + +image::budgetNormal.png[width="300"] +_Figure 7. Quote provided as a reminder when spendings are well within budget._ + +==== Editing the current budget : `editBudget` + +// tag::editbudget[] +Edits the current budget. A current budget must be present to use this command. If your budget has been edited such +that it is no longer the current budget, you can no longer edit that budget. + +Edited budget cannot overlap with other existing budgets. + [NOTE] -==== -Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). -==== +Budgets are considered to be overlapping if their active dates intersect each other. -Examples: +*Alias:* `eb` + +*Format:* `editBudget [$/AMOUNT] [p/TIME_PERIOD_IN_DAYS] [d/START_DATE]` -* `delete 1` + -`list` + -`undo` (reverses the `delete 1` command) + +image::examples.png[width="125"] -* `select 1` + -`list` + -`undo` + -The `undo` command fails as there are no undoable commands executed previously. +* `editBudget $/1000 p/7 d/01/01/2019` + +Edits the current budget to $1000 for 7 days starting from 1st January 2019. -* `delete 1` + -`clear` + -`undo` (reverses the `clear` command) + -`undo` (reverses the `delete 1` command) + +* `editBudget $/200 p/15` + +Edits the current budget to $200 for 15 days starting from the origin budget's start date. -=== Redoing the previously undone command : `redo` +// end::editbudget[] -Reverses the most recent `undo` command. + -Format: `redo` +**** +image::additionalinformation.png[width="175"] +Time period cannot exceed 1 million days. + +Budget does not take into account allowances as budget only accounts for expenses. +**** + +==== Deleting a budget : `deleteBudget` + +// tag::deletebudget[] +Deletes the budget at the specified `INDEX`. The `INDEX` refers to the number next to the status of the budget +in the displayed budget list. + +*Alias:* `db` + +*Format:* `deleteBudget INDEX` + +image::example.png[width="125"] + +* `deleteBudget 1` + +Deletes the first budget in the Budget List. +// end::deletebudget[] + +=== Goal Setting +// tag::setGoal[] +==== Setting a savings goal : `setGoal` + +Sets the item and the amount that the user wishes to save up for. + +*Alias:* `sg` + +*Format:* `setGoal n/ITEM_NAME $/AMOUNT` + +Example: + +* `setGoal n/nike shoes $/80` + +Sets the goal to a $80 Nike shoe. + +**** +image::additionalinformation.png[width="175"] +Details about the current goal and the amount required to save up to hit the current goal +can be found on the User Interface. +**** +// end::setGoal[] +// tag::report[] + +=== Expense Report + +==== Generating a report : `report` + +Generates a report of the given date, month, or year. The report consists of total inflow, total outflow, and +proportion of total expense and total allowance. + +*Alias:* `rp` + +*Formats:* + +* `report [d/DD/MM/YYYY]` + +Generates a report for specified date. + +* `report [d/MM/YYYY]` + +Generates a report for specified month. + +* `report [d/YYYY]` + +Generates a report for specified year. + +* `report` + +Generates a report of all records in ePiggy. + +[NOTE] +The message under the chart will appear only if you have at least one existing expense. + +image::examples.png[width="125"] + +* `report` + +Views the completed report of all the records. + +image::completeReport.png[width="600"] +_Figure 8: An example of a completed report._ + + +* `report d/10/04/2019` + +Views the specified date report of 10 Apr 2019. + +image::reportDay.png[width="500"] +_Figure 9: An example of a report for the specified date._ + +* `report d/04/2019` + +Views the specified month report for April 2019. + +image::reportMonth.png[width="500"] +_Figure 10: An example of a report for the specified month._ + +* `report d/2019` + +Views the specified year report for 2019. + +image::reportYear.png[width="500"] +_Figure 11: An example of a report for the specified year._ +// end::report[] + +=== Miscellaneous + +==== Cleaning the result display area: F2 + +Cleans the result display area (leftmost panel). This removes all text in the box. +Press *F2* on the keyboard or the button *"Clean Message"* on the dropdown menu to clean the result display area. + +[NOTE] +Messages cannot be restored once you clean the result display area. +Command history and other information will not be affected. -Examples: +==== Undoing previous commands : `undo` +// tag::undo[] +Returns ePiggy to the previous state. Use this if you wish to undo previous commands. + -* `delete 1` + -`undo` (reverses the `delete 1` command) + -`redo` (reapplies the `delete 1` command) + +*Format:* `undo` +// end::undo[] -* `delete 1` + -`redo` + -The `redo` command fails as there are no `undo` commands executed previously. +==== Redoing commands : `redo` +// tag::redo[] +Returns ePiggy to a previously undone state. Use this if you wish to undo an undone command (ie. get back the command). + -* `delete 1` + -`clear` + -`undo` (reverses the `clear` command) + -`undo` (reverses the `delete 1` command) + -`redo` (reapplies the `delete 1` command) + -`redo` (reapplies the `clear` command) + -// end::undoredo[] +*Format:* `redo` +// end::redo[] -=== Clearing all entries : `clear` +==== Clearing all entries : `clear` +Clears all entries (allowances, budgets, expenses and goal) from ePiggy. -Clears all entries from the address book. + -Format: `clear` +*Format:* `clear` -=== Exiting the program : `exit` +==== Exiting the program : `exit` Exits the program. + -Format: `exit` +*Alias:* `ex` + +*Format:* `exit` -=== Saving the data +==== Saving the data -Address book data are saved in the hard disk automatically after any command that changes the data. + +ePiggy's data is saved in the hard disk automatically after any command that changes the data. + There is no need to save manually. -// tag::dataencryption[] -=== Encrypting data files `[coming in v2.0]` +=== Upcoming Features -_{explain how the user can enable/disable data encryption}_ -// end::dataencryption[] +image::cominginv2.png[width="125"] + +// tag::v2.0[] +==== Registering `[coming in v2.0]` + +Allows you to register for an account in ePiggy, so that you can store your ePiggy information. + + +==== Logging in and out `[coming in v2.0]` + +Allows you to log in and out of ePiggy using your username and password, if your account exists already. +This will keep your information safe. + +This feature will be implemented only after the `register` feature has been added. + + +==== Encrypting data files `[coming in v2.0]` + +Allows you to choose to enable data encryption to secure your ePiggy information. + +==== Sharing `[coming in v2.0]` + +Allows you to share your expenses with others. ePiggy will send an email (which you input) +with an attachment of your personal ePiggy data in CSV format. + + +==== Recommend future spending `[coming in v2.0]` + +Recommends steps you can take to keep to your budget. + +ePiggy will give you 2 recommendations. First, a daily spending limit. Next, the increase in allowance you need +if you intend to maintain your current spending habits. You can choose to adopt any of the 2 recommendations. + +// end::v2.0[] == FAQ +image::frequentlyaskedquestions.png[width="200"] + *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 Address Book folder. +*A*: You can follow the steps below to transfer your data: + +1. Install _ePiggy_ in the other computer. + +2. Locate the empty 'data' folder which is in the same folder as the _ePiggy_ jar file. If there is no such folder, +run the ePiggy jar file and close it. + +3. Locate the file named _ePiggy.json_ in the _data_ folder from your previous computer. + +4. Transfer the file mentioned in _Step 3_ into the folder mentioned in _Step 2_. + +5. Your data should be transferred over successfully. You can run _ePiggy_ in your other computer to see the data. == Command Summary -* *Add* `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` + -e.g. `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -* *Clear* : `clear` -* *Delete* : `delete INDEX` + -e.g. `delete 3` -* *Edit* : `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]...` + -e.g. `edit 2 n/James Lee e/jameslee@example.com` -* *Find* : `find KEYWORD [MORE_KEYWORDS]` + -e.g. `find James Jake` -* *List* : `list` +* *Add Allowance* : `addAllowance n/ALLOWANCE_NAME $/AMOUNT [d/DATE] [t/TAG]…` + +e.g. `addAllowance n/From Mom $/20 t/School d/21/02/2019` + +* *Add Budget* : `addBudget $/AMOUNT p/TIME_PERIOD_IN_DAYS d/START_DATE` + +e.g.`addBudget $/500.00 p/7 d/01/02/2019` + +* *Add Expense* : `addExpense n/EXPENSE_NAME $/COST [d/DATE] [t/TAG]…` + +e.g. `addExpense n/Chicken rice set $/5 t/Food d/21/02/2019` + +* *Clear Data* : `clear` + +e.g. `clear` + +* *Delete Allowance* : `deleteAllowance INDEX` + +e.g. `deleteAllowance 3 + +* *Delete Budget* : `deleteBudget INDEX` + +e.g.`deleteBudget 2` + +* *Delete Expense* : `deleteExpense INDEX` + +e.g. `deleteExpense 3` + +* *Edit Allowance* : `editAllowance INDEX [n/ALLOWANCE_NAME] [$/COST] [d/DATE] [t/TAG]…` + +e.g. `editAllowance 1 n/From Mom $10 t/Emergency` + +* *Edit Budget* : `editBudget [$/AMOUNT] [p/TIME_PERIOD_IN_DAYS] [d/START_DATE]` + +e.g.`editBudget $/300.00 p/28` + +* *Edit Expense* : `editExpense INDEX [n/EXPENSE_NAME] [$/COST] [d/DATE] [t/TAG]…` + +e.g. `editExpense 1 n/Pen $/1 t/Supplies` + +* *Exit* : `exit` + +* *Find Expenses/Allowances* : `find [n/NAME] [t/TAG] [d/DATE_RANGE] [$/AMOUNT RANGE]` + +e.g.`find n/McDonalds` + * *Help* : `help` -* *Select* : `select INDEX` + -e.g.`select 2` + * *History* : `history` -* *Undo* : `undo` + +* *List* : `list` + * *Redo* : `redo` + +* *Report* : `report d/DD/MM/YYYY` + +e.g. `report d/21/03/2019` + +* *Reverse List* : `reverseList` + +* *Set Savings Goal* : `setGoal n/ITEM_NAME $/AMOUNT` + +e.g. `setGoal n/nike shoes $/80` + +* *Sort Expenses/Allowances* : `sort [n/d/$]/` + +e.g.`sort d/` + +* *Undo* : `undo` diff --git a/docs/diagrams/AddAllowanceSequenceDiagram.pptx b/docs/diagrams/AddAllowanceSequenceDiagram.pptx new file mode 100644 index 000000000000..bf6421ef94f6 Binary files /dev/null and b/docs/diagrams/AddAllowanceSequenceDiagram.pptx differ diff --git a/docs/diagrams/AddBudgetSequenceDiagram.pptx b/docs/diagrams/AddBudgetSequenceDiagram.pptx new file mode 100644 index 000000000000..73028e0c4a87 Binary files /dev/null and b/docs/diagrams/AddBudgetSequenceDiagram.pptx differ diff --git a/docs/diagrams/AddExpenseSequenceDiagram.pptx b/docs/diagrams/AddExpenseSequenceDiagram.pptx new file mode 100644 index 000000000000..2db6c2f85b6d Binary files /dev/null and b/docs/diagrams/AddExpenseSequenceDiagram.pptx differ diff --git a/docs/diagrams/ArchitectureDiagram.pptx b/docs/diagrams/ArchitectureDiagram.pptx index 13051e815c46..f1f09dcad1fb 100644 Binary files a/docs/diagrams/ArchitectureDiagram.pptx and b/docs/diagrams/ArchitectureDiagram.pptx differ diff --git a/docs/diagrams/AutoCompleteActivityDiagram.pptx b/docs/diagrams/AutoCompleteActivityDiagram.pptx new file mode 100644 index 000000000000..88bee8e6150c Binary files /dev/null and b/docs/diagrams/AutoCompleteActivityDiagram.pptx differ diff --git a/docs/diagrams/DeleteAllowanceSequenceDiagram.pptx b/docs/diagrams/DeleteAllowanceSequenceDiagram.pptx new file mode 100644 index 000000000000..1bf8979433f6 Binary files /dev/null and b/docs/diagrams/DeleteAllowanceSequenceDiagram.pptx differ diff --git a/docs/diagrams/DeleteBudgetSequenceDiagram.pptx b/docs/diagrams/DeleteBudgetSequenceDiagram.pptx new file mode 100644 index 000000000000..433d3f1767db Binary files /dev/null and b/docs/diagrams/DeleteBudgetSequenceDiagram.pptx differ diff --git a/docs/diagrams/DeleteExpenseSequenceDiagram.pptx b/docs/diagrams/DeleteExpenseSequenceDiagram.pptx new file mode 100644 index 000000000000..7d39940ee711 Binary files /dev/null and b/docs/diagrams/DeleteExpenseSequenceDiagram.pptx differ diff --git a/docs/diagrams/FindCommandSequenceDiagram.pptx b/docs/diagrams/FindCommandSequenceDiagram.pptx new file mode 100644 index 000000000000..98e033d684e4 Binary files /dev/null and b/docs/diagrams/FindCommandSequenceDiagram.pptx differ diff --git a/docs/diagrams/LogicComponentClassDiagram.pptx b/docs/diagrams/LogicComponentClassDiagram.pptx index 6fcc1136a5bb..f01dcad7d35d 100644 Binary files a/docs/diagrams/LogicComponentClassDiagram.pptx and b/docs/diagrams/LogicComponentClassDiagram.pptx differ diff --git a/docs/diagrams/LogicComponentSequenceDiagram.pptx b/docs/diagrams/LogicComponentSequenceDiagram.pptx index 410277bbd866..caf09781327b 100644 Binary files a/docs/diagrams/LogicComponentSequenceDiagram.pptx and b/docs/diagrams/LogicComponentSequenceDiagram.pptx differ diff --git a/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx b/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx index a0b23659eb29..ca366673aae4 100644 Binary files a/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx and b/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx differ diff --git a/docs/diagrams/ModelComponentClassDiagram.pptx b/docs/diagrams/ModelComponentClassDiagram.pptx index dc0e4ac5ea66..681518dd0571 100644 Binary files a/docs/diagrams/ModelComponentClassDiagram.pptx and b/docs/diagrams/ModelComponentClassDiagram.pptx differ diff --git a/docs/diagrams/ReportSequenceDiagram.pptx b/docs/diagrams/ReportSequenceDiagram.pptx new file mode 100644 index 000000000000..9d8bd1876731 Binary files /dev/null and b/docs/diagrams/ReportSequenceDiagram.pptx differ diff --git a/docs/diagrams/SetGoalSequenceDiagram.pptx b/docs/diagrams/SetGoalSequenceDiagram.pptx new file mode 100644 index 000000000000..d44f2e223d3c Binary files /dev/null and b/docs/diagrams/SetGoalSequenceDiagram.pptx differ diff --git a/docs/diagrams/SortCommandSequenceDiagram.pptx b/docs/diagrams/SortCommandSequenceDiagram.pptx new file mode 100644 index 000000000000..66e8231e42db Binary files /dev/null and b/docs/diagrams/SortCommandSequenceDiagram.pptx differ diff --git a/docs/diagrams/StorageComponentClassDiagram.pptx b/docs/diagrams/StorageComponentClassDiagram.pptx index 4afecd63e210..6632d3678c02 100644 Binary files a/docs/diagrams/StorageComponentClassDiagram.pptx and b/docs/diagrams/StorageComponentClassDiagram.pptx differ diff --git a/docs/diagrams/UiComponentClassDiagram.pptx b/docs/diagrams/UiComponentClassDiagram.pptx index ab325e889f70..d608a8f70d12 100644 Binary files a/docs/diagrams/UiComponentClassDiagram.pptx and b/docs/diagrams/UiComponentClassDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoActivityDiagram.pptx b/docs/diagrams/UndoRedoActivityDiagram.pptx index 16fec930cf3f..7a262dd327d0 100644 Binary files a/docs/diagrams/UndoRedoActivityDiagram.pptx and b/docs/diagrams/UndoRedoActivityDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoExecuteUndoStateListDiagram.pptx b/docs/diagrams/UndoRedoExecuteUndoStateListDiagram.pptx index 6fd31b5f3fbd..077734d42496 100644 Binary files a/docs/diagrams/UndoRedoExecuteUndoStateListDiagram.pptx and b/docs/diagrams/UndoRedoExecuteUndoStateListDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoNewCommand1StateListDiagram.pptx b/docs/diagrams/UndoRedoNewCommand1StateListDiagram.pptx index 1f3261976dce..b438e0c5ae5f 100644 Binary files a/docs/diagrams/UndoRedoNewCommand1StateListDiagram.pptx and b/docs/diagrams/UndoRedoNewCommand1StateListDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoNewCommand2StateListDiagram.pptx b/docs/diagrams/UndoRedoNewCommand2StateListDiagram.pptx index e2907d4a9cae..307e58e85267 100644 Binary files a/docs/diagrams/UndoRedoNewCommand2StateListDiagram.pptx and b/docs/diagrams/UndoRedoNewCommand2StateListDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoNewCommand3StateListDiagram.pptx b/docs/diagrams/UndoRedoNewCommand3StateListDiagram.pptx index 4ecc659bd600..89a016c7f3ec 100644 Binary files a/docs/diagrams/UndoRedoNewCommand3StateListDiagram.pptx and b/docs/diagrams/UndoRedoNewCommand3StateListDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoNewCommand4StateListDiagram.pptx b/docs/diagrams/UndoRedoNewCommand4StateListDiagram.pptx index 16ebf585ddbd..060c750af000 100644 Binary files a/docs/diagrams/UndoRedoNewCommand4StateListDiagram.pptx and b/docs/diagrams/UndoRedoNewCommand4StateListDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoSequenceDiagram.pptx b/docs/diagrams/UndoRedoSequenceDiagram.pptx index 5ccc1042caac..820c332b7c83 100644 Binary files a/docs/diagrams/UndoRedoSequenceDiagram.pptx and b/docs/diagrams/UndoRedoSequenceDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoStartingStateListDiagram.pptx b/docs/diagrams/UndoRedoStartingStateListDiagram.pptx index 98ce067642ff..ffbf107b773b 100644 Binary files a/docs/diagrams/UndoRedoStartingStateListDiagram.pptx and b/docs/diagrams/UndoRedoStartingStateListDiagram.pptx differ diff --git a/docs/images/AddAllowanceSequenceDiagram.png b/docs/images/AddAllowanceSequenceDiagram.png new file mode 100644 index 000000000000..e049b6290577 Binary files /dev/null and b/docs/images/AddAllowanceSequenceDiagram.png differ diff --git a/docs/images/AddExpenseSequenceDiagram.png b/docs/images/AddExpenseSequenceDiagram.png new file mode 100644 index 000000000000..348cf4ae52bb Binary files /dev/null and b/docs/images/AddExpenseSequenceDiagram.png differ diff --git a/docs/images/Architecture.png b/docs/images/Architecture.png index d9215a274f93..3473373cec42 100644 Binary files a/docs/images/Architecture.png and b/docs/images/Architecture.png differ diff --git a/docs/images/AutocompleteActivityDiagram.png b/docs/images/AutocompleteActivityDiagram.png new file mode 100644 index 000000000000..74ffd989cde7 Binary files /dev/null and b/docs/images/AutocompleteActivityDiagram.png differ diff --git a/docs/images/BudgetUi.png b/docs/images/BudgetUi.png new file mode 100644 index 000000000000..705594b369d3 Binary files /dev/null and b/docs/images/BudgetUi.png differ diff --git a/docs/images/DeleteAllowanceSequenceDiagram.png b/docs/images/DeleteAllowanceSequenceDiagram.png new file mode 100644 index 000000000000..902a79a4e290 Binary files /dev/null and b/docs/images/DeleteAllowanceSequenceDiagram.png differ diff --git a/docs/images/DeleteBudgetSequenceDiagram.png b/docs/images/DeleteBudgetSequenceDiagram.png new file mode 100644 index 000000000000..47273f02058a Binary files /dev/null and b/docs/images/DeleteBudgetSequenceDiagram.png differ diff --git a/docs/images/DeleteExpenseSdForLogic.png b/docs/images/DeleteExpenseSdForLogic.png new file mode 100644 index 000000000000..631dc6f7f07b Binary files /dev/null and b/docs/images/DeleteExpenseSdForLogic.png differ diff --git a/docs/images/DeletePersonSdForLogic.png b/docs/images/DeletePersonSdForLogic.png deleted file mode 100644 index 0462b9b7be6e..000000000000 Binary files a/docs/images/DeletePersonSdForLogic.png and /dev/null differ diff --git a/docs/images/Firstlaunch.png b/docs/images/Firstlaunch.png new file mode 100644 index 000000000000..907f9b7a12a0 Binary files /dev/null and b/docs/images/Firstlaunch.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index f4ecf65b3193..8d46ac0cb462 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/ModelClassBetterOopDiagram.png b/docs/images/ModelClassBetterOopDiagram.png index b7df3a1c02b4..44e84940372a 100644 Binary files a/docs/images/ModelClassBetterOopDiagram.png and b/docs/images/ModelClassBetterOopDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 4961edd74e76..d6ce51013fa2 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/SDforDeleteExpense.png b/docs/images/SDforDeleteExpense.png new file mode 100644 index 000000000000..650d2911d793 Binary files /dev/null and b/docs/images/SDforDeleteExpense.png differ diff --git a/docs/images/SDforDeletePerson.png b/docs/images/SDforDeletePerson.png deleted file mode 100644 index ae171fda7622..000000000000 Binary files a/docs/images/SDforDeletePerson.png and /dev/null differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index e5527ecac459..487be8dae7a4 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5ec9c527b49c..f4a0e9a8eb49 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 5f3847621e07..a861de757476 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UiEmpty.png b/docs/images/UiEmpty.png new file mode 100644 index 000000000000..7273ca77fe06 Binary files /dev/null and b/docs/images/UiEmpty.png differ diff --git a/docs/images/UndoRedoActivityDiagram.png b/docs/images/UndoRedoActivityDiagram.png index 55e4138cc64f..7e70c2ec444b 100644 Binary files a/docs/images/UndoRedoActivityDiagram.png and b/docs/images/UndoRedoActivityDiagram.png differ diff --git a/docs/images/UndoRedoExecuteUndoStateListDiagram.png b/docs/images/UndoRedoExecuteUndoStateListDiagram.png index 29c365d6b4a1..ee16b7184c6a 100644 Binary files a/docs/images/UndoRedoExecuteUndoStateListDiagram.png and b/docs/images/UndoRedoExecuteUndoStateListDiagram.png differ diff --git a/docs/images/UndoRedoNewCommand1StateListDiagram.png b/docs/images/UndoRedoNewCommand1StateListDiagram.png index 76e661d62027..7e2ee6e950ad 100644 Binary files a/docs/images/UndoRedoNewCommand1StateListDiagram.png and b/docs/images/UndoRedoNewCommand1StateListDiagram.png differ diff --git a/docs/images/UndoRedoNewCommand2StateListDiagram.png b/docs/images/UndoRedoNewCommand2StateListDiagram.png index adcb9aeadc51..6805d7d11831 100644 Binary files a/docs/images/UndoRedoNewCommand2StateListDiagram.png and b/docs/images/UndoRedoNewCommand2StateListDiagram.png differ diff --git a/docs/images/UndoRedoNewCommand3StateListDiagram.png b/docs/images/UndoRedoNewCommand3StateListDiagram.png index aac9c5fe05db..24fd9fa37043 100644 Binary files a/docs/images/UndoRedoNewCommand3StateListDiagram.png and b/docs/images/UndoRedoNewCommand3StateListDiagram.png differ diff --git a/docs/images/UndoRedoNewCommand4StateListDiagram.png b/docs/images/UndoRedoNewCommand4StateListDiagram.png index 66a0a3b5f323..233c8bf5a708 100644 Binary files a/docs/images/UndoRedoNewCommand4StateListDiagram.png and b/docs/images/UndoRedoNewCommand4StateListDiagram.png differ diff --git a/docs/images/UndoRedoSequenceDiagram.png b/docs/images/UndoRedoSequenceDiagram.png index 5c9d5936f098..4d43ecd580fa 100644 Binary files a/docs/images/UndoRedoSequenceDiagram.png and b/docs/images/UndoRedoSequenceDiagram.png differ diff --git a/docs/images/UndoRedoStartingStateListDiagram.png b/docs/images/UndoRedoStartingStateListDiagram.png index 002f3e2bbf79..121129805923 100644 Binary files a/docs/images/UndoRedoStartingStateListDiagram.png and b/docs/images/UndoRedoStartingStateListDiagram.png differ diff --git a/docs/images/about.png b/docs/images/about.png new file mode 100644 index 000000000000..4451eabf4a5d Binary files /dev/null and b/docs/images/about.png differ diff --git a/docs/images/addBudgetSequenceDiagram.png b/docs/images/addBudgetSequenceDiagram.png new file mode 100644 index 000000000000..2aa7daa5db8c Binary files /dev/null and b/docs/images/addBudgetSequenceDiagram.png differ diff --git a/docs/images/additionalinformation.png b/docs/images/additionalinformation.png new file mode 100644 index 000000000000..3e10277dbc44 Binary files /dev/null and b/docs/images/additionalinformation.png differ diff --git a/docs/images/allowance.PNG b/docs/images/allowance.PNG new file mode 100644 index 000000000000..92d5c084e4b9 Binary files /dev/null and b/docs/images/allowance.PNG differ diff --git a/docs/images/budget80percent.png b/docs/images/budget80percent.png new file mode 100644 index 000000000000..218aa9b4ccf6 Binary files /dev/null and b/docs/images/budget80percent.png differ diff --git a/docs/images/budgetEmpty.png b/docs/images/budgetEmpty.png new file mode 100644 index 000000000000..6720d60d9e94 Binary files /dev/null and b/docs/images/budgetEmpty.png differ diff --git a/docs/images/budgetExceeded.png b/docs/images/budgetExceeded.png new file mode 100644 index 000000000000..90bb5efd3414 Binary files /dev/null and b/docs/images/budgetExceeded.png differ diff --git a/docs/images/budgetNormal.png b/docs/images/budgetNormal.png new file mode 100644 index 000000000000..7e7cf19e6807 Binary files /dev/null and b/docs/images/budgetNormal.png differ diff --git a/docs/images/budgetstatus.png b/docs/images/budgetstatus.png new file mode 100644 index 000000000000..9bf99a7ba809 Binary files /dev/null and b/docs/images/budgetstatus.png differ diff --git a/docs/images/callouts.png b/docs/images/callouts.png new file mode 100644 index 000000000000..9147e30c8be1 Binary files /dev/null and b/docs/images/callouts.png differ diff --git a/docs/images/cominginv2.png b/docs/images/cominginv2.png new file mode 100644 index 000000000000..a56f4558098e Binary files /dev/null and b/docs/images/cominginv2.png differ diff --git a/docs/images/commandformat.png b/docs/images/commandformat.png new file mode 100644 index 000000000000..66cac9ee6dee Binary files /dev/null and b/docs/images/commandformat.png differ diff --git a/docs/images/completeReport.png b/docs/images/completeReport.png new file mode 100644 index 000000000000..4bdba7c1cfca Binary files /dev/null and b/docs/images/completeReport.png differ diff --git a/docs/images/damithc.jpg b/docs/images/damithc.jpg deleted file mode 100644 index 127543883893..000000000000 Binary files a/docs/images/damithc.jpg and /dev/null differ diff --git a/docs/images/default1.png b/docs/images/default1.png new file mode 100644 index 000000000000..e2e84e9b7f52 Binary files /dev/null and b/docs/images/default1.png differ diff --git a/docs/images/developerguide.png b/docs/images/developerguide.png new file mode 100644 index 000000000000..c738e3a41a6f Binary files /dev/null and b/docs/images/developerguide.png differ diff --git a/docs/images/developerguidedesign.png b/docs/images/developerguidedesign.png new file mode 100644 index 000000000000..84e6d0fa6120 Binary files /dev/null and b/docs/images/developerguidedesign.png differ diff --git a/docs/images/developerguidedevops.png b/docs/images/developerguidedevops.png new file mode 100644 index 000000000000..43dba9f9eea1 Binary files /dev/null and b/docs/images/developerguidedevops.png differ diff --git a/docs/images/developerguidedocumentation.png b/docs/images/developerguidedocumentation.png new file mode 100644 index 000000000000..b2e47ba2025b Binary files /dev/null and b/docs/images/developerguidedocumentation.png differ diff --git a/docs/images/developerguideheaders.png b/docs/images/developerguideheaders.png new file mode 100644 index 000000000000..0195ec871b37 Binary files /dev/null and b/docs/images/developerguideheaders.png differ diff --git a/docs/images/developerguideimplementation.png b/docs/images/developerguideimplementation.png new file mode 100644 index 000000000000..2375bd877114 Binary files /dev/null and b/docs/images/developerguideimplementation.png differ diff --git a/docs/images/developerguidesettingup.png b/docs/images/developerguidesettingup.png new file mode 100644 index 000000000000..c482255e9a36 Binary files /dev/null and b/docs/images/developerguidesettingup.png differ diff --git a/docs/images/developerguidetesting.png b/docs/images/developerguidetesting.png new file mode 100644 index 000000000000..5109ce2a25f6 Binary files /dev/null and b/docs/images/developerguidetesting.png differ diff --git a/docs/images/example.png b/docs/images/example.png new file mode 100644 index 000000000000..7b682a410f69 Binary files /dev/null and b/docs/images/example.png differ diff --git a/docs/images/examples.png b/docs/images/examples.png new file mode 100644 index 000000000000..a742999d0229 Binary files /dev/null and b/docs/images/examples.png differ diff --git a/docs/images/extract.png b/docs/images/extract.png new file mode 100644 index 000000000000..60fe9f4a2694 Binary files /dev/null and b/docs/images/extract.png differ diff --git a/docs/images/fEuml.png b/docs/images/fEuml.png new file mode 100644 index 000000000000..515aaf91d087 Binary files /dev/null and b/docs/images/fEuml.png differ diff --git a/docs/images/frequentlyaskedquestions.png b/docs/images/frequentlyaskedquestions.png new file mode 100644 index 000000000000..fed43b25c8a8 Binary files /dev/null and b/docs/images/frequentlyaskedquestions.png differ diff --git a/docs/images/highlights.png b/docs/images/highlights.png new file mode 100644 index 000000000000..c8f3be3181b0 Binary files /dev/null and b/docs/images/highlights.png differ diff --git a/docs/images/justification.png b/docs/images/justification.png new file mode 100644 index 000000000000..e2ae454928cc Binary files /dev/null and b/docs/images/justification.png differ diff --git a/docs/images/kev-inc.png b/docs/images/kev-inc.png new file mode 100644 index 000000000000..4d14f5e1b582 Binary files /dev/null and b/docs/images/kev-inc.png differ diff --git a/docs/images/lejolly.jpg b/docs/images/lejolly.jpg deleted file mode 100644 index 2d1d94e0cf5d..000000000000 Binary files a/docs/images/lejolly.jpg and /dev/null differ diff --git a/docs/images/m133225.jpg b/docs/images/m133225.jpg deleted file mode 100644 index fd14fb94593a..000000000000 Binary files a/docs/images/m133225.jpg and /dev/null differ diff --git a/docs/images/overview.png b/docs/images/overview.png new file mode 100644 index 000000000000..56e0647f1864 Binary files /dev/null and b/docs/images/overview.png differ diff --git a/docs/images/pdnm.png b/docs/images/pdnm.png new file mode 100644 index 000000000000..b413af2999a4 Binary files /dev/null and b/docs/images/pdnm.png differ diff --git a/docs/images/ppplogo.png b/docs/images/ppplogo.png new file mode 100644 index 000000000000..c5b81c10311b Binary files /dev/null and b/docs/images/ppplogo.png differ diff --git a/docs/images/projectportfolioheader.png b/docs/images/projectportfolioheader.png new file mode 100644 index 000000000000..43dc3bd6dd8e Binary files /dev/null and b/docs/images/projectportfolioheader.png differ diff --git a/docs/images/projectteam.png b/docs/images/projectteam.png new file mode 100644 index 000000000000..8bf6b807b1fb Binary files /dev/null and b/docs/images/projectteam.png differ diff --git a/docs/images/rLuml.png b/docs/images/rLuml.png new file mode 100644 index 000000000000..584af1ab741d Binary files /dev/null and b/docs/images/rLuml.png differ diff --git a/docs/images/rahulb99.png b/docs/images/rahulb99.png new file mode 100644 index 000000000000..2d5c8a7da463 Binary files /dev/null and b/docs/images/rahulb99.png differ diff --git a/docs/images/reportDay.png b/docs/images/reportDay.png new file mode 100644 index 000000000000..118e831031d0 Binary files /dev/null and b/docs/images/reportDay.png differ diff --git a/docs/images/reportMonth.png b/docs/images/reportMonth.png new file mode 100644 index 000000000000..f6800126b3b4 Binary files /dev/null and b/docs/images/reportMonth.png differ diff --git a/docs/images/reportSequenceDiagram.png b/docs/images/reportSequenceDiagram.png new file mode 100644 index 000000000000..f156eca3fcb8 Binary files /dev/null and b/docs/images/reportSequenceDiagram.png differ diff --git a/docs/images/reportYear.png b/docs/images/reportYear.png new file mode 100644 index 000000000000..0bf65f6a77bf Binary files /dev/null and b/docs/images/reportYear.png differ diff --git a/docs/images/sEuml.png b/docs/images/sEuml.png new file mode 100644 index 000000000000..8d5675a685ec Binary files /dev/null and b/docs/images/sEuml.png differ diff --git a/docs/images/samplecommands.png b/docs/images/samplecommands.png new file mode 100644 index 000000000000..06a3e470ab08 Binary files /dev/null and b/docs/images/samplecommands.png differ diff --git a/docs/images/setGoalSequenceDiagram.PNG b/docs/images/setGoalSequenceDiagram.PNG new file mode 100644 index 000000000000..b6fce4baad1a Binary files /dev/null and b/docs/images/setGoalSequenceDiagram.PNG differ diff --git a/docs/images/tehwenyi.png b/docs/images/tehwenyi.png new file mode 100644 index 000000000000..a557cea8fdc5 Binary files /dev/null and b/docs/images/tehwenyi.png differ diff --git a/docs/images/userguide.png b/docs/images/userguide.png new file mode 100644 index 000000000000..7b4011d78563 Binary files /dev/null and b/docs/images/userguide.png differ diff --git a/docs/images/userguideheaders.png b/docs/images/userguideheaders.png new file mode 100644 index 000000000000..c05e16224717 Binary files /dev/null and b/docs/images/userguideheaders.png differ diff --git a/docs/images/userguideintroduction.gif b/docs/images/userguideintroduction.gif new file mode 100644 index 000000000000..866a927ddebc Binary files /dev/null and b/docs/images/userguideintroduction.gif differ diff --git a/docs/images/whatitdoes.png b/docs/images/whatitdoes.png new file mode 100644 index 000000000000..9d464c943940 Binary files /dev/null and b/docs/images/whatitdoes.png differ diff --git a/docs/images/yijinl.jpg b/docs/images/yijinl.jpg deleted file mode 100644 index adbf62ad9406..000000000000 Binary files a/docs/images/yijinl.jpg and /dev/null differ diff --git a/docs/images/yl_coder.jpg b/docs/images/yl_coder.jpg deleted file mode 100644 index 17b48a732272..000000000000 Binary files a/docs/images/yl_coder.jpg and /dev/null differ diff --git a/docs/images/yunjun199321.png b/docs/images/yunjun199321.png new file mode 100644 index 000000000000..7fd97c9d1129 Binary files /dev/null and b/docs/images/yunjun199321.png differ diff --git a/docs/stylesheets/gh-pages.css b/docs/stylesheets/gh-pages.css index 121cac3885fd..d8c498b05372 100644 --- a/docs/stylesheets/gh-pages.css +++ b/docs/stylesheets/gh-pages.css @@ -1,4 +1,5 @@ @import url(https://fonts.googleapis.com/css?family=Montserrat|Open+Sans); +@import "style.css"; @import "asciidoctor.css"; /* Default asciidoc style framework - important */ /* Custom block: details */ diff --git a/docs/stylesheets/style.css b/docs/stylesheets/style.css new file mode 100644 index 000000000000..998e5ce61e3b --- /dev/null +++ b/docs/stylesheets/style.css @@ -0,0 +1,5 @@ +@media print { + a[href]:after { + content: none !important; + } +} diff --git a/docs/team/johndoe.adoc b/docs/team/johndoe.adoc deleted file mode 100644 index 453c2152ab9d..000000000000 --- a/docs/team/johndoe.adoc +++ /dev/null @@ -1,72 +0,0 @@ -= John Doe - Project Portfolio -:site-section: AboutUs -:imagesDir: ../images -:stylesDir: ../stylesheets - -== PROJECT: AddressBook - Level 4 - ---- - -== Overview - -AddressBook - Level 4 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. - -== Summary of contributions - -* *Major enhancement*: added *the ability to undo/redo previous commands* -** What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. -** Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. -** Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. -** Credits: _{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}_ - -* *Minor enhancement*: added a history command that allows the user to navigate to previous commands using up/down keys. - -* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ - -* *Other contributions*: - -** Project management: -*** Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub -** Enhancements to existing features: -*** Updated the GUI color scheme (Pull requests https://github.com[#33], https://github.com[#34]) -*** Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests https://github.com[#36], https://github.com[#38]) -** Documentation: -*** Did cosmetic tweaks to existing contents of the User Guide: https://github.com[#14] -** Community: -*** PRs reviewed (with non-trivial review comments): https://github.com[#12], https://github.com[#32], https://github.com[#19], https://github.com[#42] -*** Contributed to forum discussions (examples: https://github.com[1], https://github.com[2], https://github.com[3], https://github.com[4]) -*** Reported bugs and suggestions for other teams in the class (examples: https://github.com[1], https://github.com[2], https://github.com[3]) -*** Some parts of the history feature I added was adopted by several other class mates (https://github.com[1], https://github.com[2]) -** Tools: -*** Integrated a third party library (Natty) to the project (https://github.com[#42]) -*** Integrated a new Github plugin (CircleCI) to the team repo - -_{you can add/remove categories in the list above}_ - -== Contributions to the User Guide - - -|=== -|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ -|=== - -include::../UserGuide.adoc[tag=undoredo] - -include::../UserGuide.adoc[tag=dataencryption] - -== Contributions to the Developer Guide - -|=== -|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ -|=== - -include::../DeveloperGuide.adoc[tag=undoredo] - -include::../DeveloperGuide.adoc[tag=dataencryption] - - -== PROJECT: PowerPointLabs - ---- - -_{Optionally, you may include other projects in your portfolio.}_ diff --git a/docs/team/kevin.adoc b/docs/team/kevin.adoc new file mode 100644 index 000000000000..2fac83ed8a5f --- /dev/null +++ b/docs/team/kevin.adoc @@ -0,0 +1,134 @@ += Kevin Chan - Project Portfolio for ePiggy +:site-section: AboutUs +:imagesDir: ../images +:sectnums: +:stylesDir: ../stylesheets + +image::ppplogo.png[width="300"] +This portfolio serves to document my contributions to https://github.com/CS2103-AY1819S2-W17-4/main[*_ePiggy_*]. + + +== Overview of *_ePiggy_* +*_ePiggy_* is a simplified expense tracker for students. +It is used to record allowances, expenses, goals, and budgets. +It has student-centric features that makes managing their money easier. + +*_ePiggy_* was build based of a basic command line interface AddressBook for a software engineering project. +My team and I decided to build ePiggy because we wanted to teach students to start managing their expenses from young, through the use of basic coding, and in a manner that relates to them. + +My primary role was to develop the allowance and goal setting feature. +The following sections show these enhancements in more detail, as well as +additional features that I added to the user and developer guides on top of these enhancements. + +== Summary of contributions +This section shows a summary of my code, documentation, and other contributions to the team project. + +=== Major Enhancements +I implemented the `Allowance` feature which comprises of 3 commands: `addAllowance`, `editAllowance`, and `deleteAllowance`. +[none] +* *What it does:* +** The 3 commands allow the user to record inflows of money (mainly allowances from parents +since our target audience is students) +* *Justification:* +** By recording down the allowances that the user receives, *_ePiggy_* will be able to calculate the +net amount of money that the user has. Knowing this information will be useful to the user. +** Apart from the user, knowing the net amount of money is also crucial to the savings goal feature +which will be further explained below. +* *Highlights:* +** Together with the `Expense` feature, the `Allowance` feature is one of the main functions that +is the core of *_ePiggy_*. +** I worked together with my team member Minh, who was in charge of the `Expense` feature, to +integrate `Expenses` and `Allowances` together seamlessly in the calculation of net savings. +** Extensive testing of the `Allowance` feature is required to ensure that the basic function of +*_ePiggy_* is working. + +=== Minor Enhancements +I also implemented the goal setting feature, which comprises of the `setGoal` command. +[none] +* *What it does:* +** `setGoal` allows the user to set the name and the amount of their savings goal. +** The savings goal will be displayed on the `SavingsPanel` of the User Interface, together +with the amount the user has to save up in order to reach the goal. +* *Justification:* +** This is one of the student-centric features that is one of the main selling points of *_ePiggy_*. +** By allowing users to set a goal, and to show the difference between their current savings +and goal, we hope to motivate users to save up enough money to buy the things they want, instilling +the value of saving money at a young age. +* *Highlights:* +** This feature requires an accurate calculation of net savings in order to calculate the +amount required for the user to save up to reach the savings goal. + +=== Code Contributed +Over the past 4 months, I have contributed over 8000 lines of code, with over 100 commits. +Samples of my code can be viewed using the links below: + +* Functional code: +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/AddAllowanceCommand.java[AddAllowanceCommand] +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/DeleteAllowanceCommand.java[DeleteAllowanceCommand] +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/EditAllowanceCommand.java[EditAllowanceCommnand] +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/SetGoalCommand.java[SetGoalCommand] +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/SavingsPanel.java[SavingsPanel] +* Test code: +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/AddAllowanceCommandTest.java[AddAllowanceCommandTest] +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/DeleteAllowanceCommandTest.java[DeleteAllowanceCommandTest] +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/EditAllowanceCommandTest.java[EditAllowanceCommandTest] +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/SetGoalCommandTest.java[SetGoalCommandTest] +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/systemtests/epiggy/AddAllowanceCommandSystemTest.java[AddAllowanceCommandSystemTest] +* All my contributions can be viewed https://nus-cs2103-ay1819s2.github.io/cs2103-dashboard/#=undefined&search=kev-inc[here] + +=== Other Contributions + +* Refactored `Storage` to include `Expense`, `Goal`, `Savings` and `Budget`. +https://github.com/CS2103-AY1819S2-W17-4/main/commit/beee7c5ca1e6431496a45a2040b80d209cb47d6b[(#132)] +https://github.com/CS2103-AY1819S2-W17-4/main/commit/4cf13e8e226c780e645646b4805fa59739d9fafd[(#168)] +* Shifted the `CommandBox` and `ResultDisplay` to the side of the layout to achieve a chatbot layout. +https://github.com/CS2103-AY1819S2-W17-4/main/pull/218[(#218)] +* Created a `SavingsPanel` for users to view current savings and goals in the user interface. +https://github.com/CS2103-AY1819S2-W17-4/main/commit/d7de3ec090cfd9a698e85e869554237cf1fb3788[(#9)] +* Added command aliases. +https://github.com/CS2103-AY1819S2-W17-4/main/pull/3/commits/0a5ae592242acf47490f200b5efe034373e8aac8[(#3)] +* Helped to fix bugs in classes created by other teammates. +https://github.com/CS2103-AY1819S2-W17-4/main/commit/f0d99ad433d3741491aafcc6798527407b4f5191[(#97)] +* Compiled and made formatting changes to the user guide to make it more reader-friendly https://github.com/CS2103-AY1819S2-W17-4/main/commit/5046afedac24c4da8a391b84a5a767604efa2155[(#85)] +* Set up auto-publishing of documentation for the team +* Set up reposense for the team +https://github.com/CS2103-AY1819S2-W17-4/main/commit/351dcddd54343100950d9ad359f92b9ce98ba91a[(#6)] + +== Contributions to the User Guide +We had to write a new User Guide as most of the sections in the AddressBook User Guide was not applicable to our application. +Below is an excerpt from the *ePiggy User Guide*, showing additions I made for the `addAllowance`, `deleteAllowance`, `editAllowance` and `setGoal` commands. +They showcase my ability to write user manuals targeting end-users. + +The full User Guide for *_ePiggy_* can be found https://cs2103-ay1819s2-w17-4.github.io/main/UserGuide.html[here]. + +include::../UserGuide.adoc[tag=aa] + +include::../UserGuide.adoc[tag=da] + +include::../UserGuide.adoc[tag=ea] + +include::../UserGuide.adoc[tag=setGoal] + +== Contributions to the Developer Guide +The following section shows my additions to the developer guide for the `addAllowance`, `deleteAllowance`, `editAllowance` and `setGoal` commands. +They showcase the technical depth of my contributions as well as the ability to convey +technical information to other developers clearly. + +The full developer guide for *_ePiggy_* can be found https://cs2103-ay1819s2-w17-4.github.io/main/DeveloperGuide.html[here]. + +[none] +=== Feature Implementation + +include::../DeveloperGuide.adoc[tag=aa] + +include::../DeveloperGuide.adoc[tag=da] + +include::../DeveloperGuide.adoc[tag=ea] + +include::../DeveloperGuide.adoc[tag=setGoal] + +[none] +=== Instructions for Manual Testing + +include::../DeveloperGuide.adoc[tag=manualtestingallowance] + +include::../DeveloperGuide.adoc[tag=manualtestinggoal] diff --git a/docs/team/minh.adoc b/docs/team/minh.adoc new file mode 100644 index 000000000000..9d9743f4035f --- /dev/null +++ b/docs/team/minh.adoc @@ -0,0 +1,101 @@ += Phan Duc Nhat Minh - Project Portfolio for ePiggy +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: ePiggy + +This portfolio aims to document my contributions to *_ePiggy_*. +*_ePiggy_* Github link: https://github.com/CS2103-AY1819S2-W17-4/main + +--- + +== Introduction +=== About +// what specific project task is, what module it is for, whether it is an indiv or grp proj +*ePiggy* is a desktop application for managing personal finances. It allows users to track expenses and allowances, +manage budgets and saving goals. The app is designed specifically for students to encourage them to establish +good spending habits. + + +*ePiggy* features a minimalist Graphical User Interface, and it interacts with users through a Command Line Interface. +Written in Java, it can be used on all major desktop operating systems, including Windows, Mac OS, and Linux. + +=== Project Team + +*ePiggy* was developed as a group project for the twin modules +_CS2101 - Effective Communication for Computing Professionals_ +and _CS2103T - Software Engineering_. It was transformed from +the link:https://github.com/nus-cs2103-AY1819S2/addressbook-level4[AddressBook - Level 4] application. +My team consists of 5 members, including myself. + +I implemented the expense tracking features (adding, deleting and modifying expense records). + +== Summary of contributions + +=== Features implemented + +I implemented the expense management features. + +image::whatitdoes.png[width="150"] + +These features allow users to manage expenses by adding, modifying and deleting expenses. +It provides a panel in the User Interface for viewing the expenses. + +image::justification.png[width="150"] + +These features provide the essential functionality for an expense tracker application. +The users have flexible control over the records, as they can modifying existing records and remove +them. With the clean expense list in the UI, they can view any past records easily. + +image::highlights.png[width="150"] + +As these are the essential features of the product, I was also responsible for designing the model that +forms the basis for other features to be implemented. +The implementation involved changes in all components of the system, requiring me to have +a thorough understanding of the whole codebase. + +=== Code Contributed +As of 15th April 2019, I have contributed 20 Pull Requests and reviewed many Pull Requests. + +* *Functional code:* link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/AddExpenseCommand.java[Add Expense] +| link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/EditExpenseCommand.java[Edit Expense] +| link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/DeleteExpenseCommand.java[Delete Expense] +| link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ExpenseListPanel.java[Expense Panel] +| link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ExpenseCard.java[Expense Card] + +* *Test code:* link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/systemtests/epiggy/AddExpenseCommandSystemTest.java[Add Expense] + +View all the code I contributed link:https://nus-cs2103-ay1819s2.github.io/cs2103-dashboard/#search=pdnm&sort=displayName&since=2019-02-10&until=2019-04-14&timeframe=day&reverse=false&repoSort=true[here]! + + +=== Other Contributions + +* Project Management: +As I am the team leader, I am in charge of the approach and direction for our project. +This includes deciding how we go about morphing the product, and how we work collaboratively. + + +* Undo / Redo: I am in charge of the Undo / Redo feature. Since the feature was already implemented +in AddressBook 4, my task was to help others integrate their features with Undo / Redo. + +== Contributions to the User Guide + +Click link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/docs/UserGuide.adoc[here] to view the full User Guide! + +I am responsible for the user guide of the commands that I implemented, which are +`addExpense`, `editExpense` and `deleteExpense`. + +include::../UserGuide.adoc[tag=aed] + +== Contributions to the Developer Guide + +Click link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/docs/DeveloperGuide.adoc[here] to view the full +Developer Guide! + +The following section shows my additions to the developer guide for the model component design and +the expense tracking features. + +include::../DeveloperGuide.adoc[tag=model] + +include::../DeveloperGuide.adoc[tag=ae] + +include::../DeveloperGuide.adoc[tag=ee] + +include::../DeveloperGuide.adoc[tag=de] diff --git a/docs/team/rahulb99.adoc b/docs/team/rahulb99.adoc new file mode 100644 index 000000000000..201e19e7586e --- /dev/null +++ b/docs/team/rahulb99.adoc @@ -0,0 +1,175 @@ += Rahul Baid - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +image::rahulb99.png[width="100", align="left"] + + +This portfolio aims to document my contributions to *_ePiggy_*. + +*_ePiggy_* Github link: https://github.com/CS2103-AY1819S2-W17-4/main + +--- + +== PROJECT: ePiggy + +--- + +== Overview + +_ePiggy_ is a desktop application designed to inculcate good spending habits in students. +_ePiggy_ hopes to make managing money much simpler for you with a simple interface and simple commands! +At the same time, _ePiggy_ offers everything from tracking expenses and managing budgets to setting goals! + + +The user interacts with it using a CLI, and it has a GUI created with JavaFX. It can be used on both Windows and Mac. + + +*_ePiggy_* is a product of twin modules CS2101 and CS2103T. It was transformed from the +link:https://github.com/nus-cs2103-AY1819S2/addressbook-level4[AddressBook - Level 4] application. + +== Summary of contributions + +--- +This section summarises my contribution to _ePiggy_'s codebase including the newly added features and improvements over +_Address Book - Level 4_ application. + +* *Major enhancements*: added *the ability to search and sort for expenses* +** What it does: + + 1. allows the user to search for his/her expenses by either it's `name`, `cost`, `date` and/or `tag` ! + + 2. allows the user to sort his/her expenses by either it's `name`, `cost` or `date` ! +** Justification: These features improves the product significantly because: + +1. It becomes more convenient for the user to find a particular expenses or a set of expenses +to figure out where he/she has been spending. +2. It becomes more convenient for the user to sort the expenses according to his/her preferences. +For example, if the user wants to find out where he/she has been spending more, the user can sort it by cost. +** Highlights: These enhancements are two of the most user-friendly commands in _ePiggy_, mainly due to their need and adaptability. + +1. The search feature enhancement have been built on top of the existing functionality. Now it allows multiple keywords search to filter out the +expenses. This feature even searches for similar words in case the user makes a typing error. Furthermore, it allows user to +enter a range for date and cost. This implementation was quite challenging as it required the complete modification of the existing command. +2. The sort feature enhancement was built from scratch. It required an in-depth analysis of design alternatives. +This new feature greatly adds to the functionality of existing commands (that were modified from address-book level 4). + +* *Minor enhancement*: Added reverse list command. Modified existing `help`, `history` and `list` commands. Refactored the codebase. +Added UI features for `expense`. [https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ExpenseCard.java[1]], +[https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ExpenseListPanel.java[2]], +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/HelpWindow.java[3]] . + +* *Functional code contributed*: [https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/FindExpenseCommand.java[FindExpense]], +[https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/SortExpenseCommand.java[SortExpense]], +[https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/ReverseListCommand.java[ReverseList]] . + +* *Test code contributed*: [https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/FindCommandTest.java[FindCommandTest]], +[https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/ReverseListCommandTest.java[ReverseListCommandTest]], +[https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/SortCommandTest.java[SortCommandTest]], + [https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/ReverseListCommandTest.java[ReverseListCommandTest]], + [https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/systemtests/epiggy/FindCommandSystemTest.java[FindCommandSystemTest]], + [https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/systemtests/epiggy/SortCommandSystemTest.java[SortCommandSystemTest]], + Parser Tests - [https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/parser/epiggy/FindCommandParserTest.java[1]], + [https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/parser/epiggy/SortCommandParserTest.java[2]]. + +* *Code contributed*: [https://nus-cs2103-ay1819s2.github.io/cs2103-dashboard/#=undefined&search=rahulb99[Reposense]] + +* *Other contributions*: + +** Project management: +*** Managed product releases on GitHub. +*** Set up the issue tracker, user stories, and milestones. +*** Assigned bugs from other teams to respective team members. +*** Extensively tested for bugs for all features of `EPiggy`. + +** Enhancements to existing features: +*** Made `date` an optional keyword while adding an expense. + (so now, if the `date` keyword is missing, `date` takes the value of device's date). (Commit: https://github.com/CS2103-AY1819S2-W17-4/main/pull/135/commits/b974cc12cc700d751de34c80307267d26f5e455e[b974cc] ) +*** Wrote base tests for any command related to `expense` such as [https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/testutil/epiggy/ExpenseUtil.java[1]], + [https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/testutil/epiggy/ExpensesBuilder.java[2]], + [https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/testutil/epiggy/TypicalExpenses.java[3]] in addition to `FindCommandTest`, `SortCommandTest` and `ReverseListCommandTest`. + (Pull requests https://github.com/CS2103-AY1819S2-W17-4/main/pull/135[#135], https://github.com/CS2103-AY1819S2-W17-4/main/pull/160[#160]) +*** Modified the search engine by make it more powerful by searching for similar words in case user makes a typo, or even when user +types a part of the keyword he/she wishes to search for. +*** Modified the storage functionality. (Pull Request https://github.com/CS2103-AY1819S2-W17-4/main/pull/20[#20]) + +** Documentation: +*** Did some UI changes to `index.html` (main page of our application). +*** Formalised the documentation of _ePiggy_'s Developer Guide. + +** Community: +*** PRs reviewed (https://github.com/CS2103-AY1819S2-W17-4/main/pull/40[#40], https://github.com/CS2103-AY1819S2-W17-4/main/pull/94[#94], +https://github.com/CS2103-AY1819S2-W17-4/main/pull/97[#97], https://github.com/CS2103-AY1819S2-W17-4/main/pull/115[#115], +https://github.com/CS2103-AY1819S2-W17-4/main/pull/123[#123], https://github.com/CS2103-AY1819S2-W17-4/main/pull/40[#132], +https://github.com/CS2103-AY1819S2-W17-4/main/pull/139[#139], https://github.com/CS2103-AY1819S2-W17-4/main/pull/150[#150], +https://github.com/CS2103-AY1819S2-W17-4/main/pull/151[#151], https://github.com/CS2103-AY1819S2-W17-4/main/pull/153[#153], +https://github.com/CS2103-AY1819S2-W17-4/main/pull/157[#157], https://github.com/CS2103-AY1819S2-W17-4/main/pull/218[#218], +https://github.com/CS2103-AY1819S2-W17-4/main/pull/257[#257], https://github.com/CS2103-AY1819S2-W17-4/main/pull/258[#258], +https://github.com/CS2103-AY1819S2-W17-4/main/pull/259[#259], https://github.com/CS2103-AY1819S2-W17-4/main/pull/265[#265]) + +*** Contributed to forum discussion (example: https://github.com/nus-cs2103-AY1819S2/forum/issues/64[#64]) + +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com/CS2103-AY1819S2-W14-1/main/issues/151[#151], +https://github.com/CS2103-AY1819S2-W14-1/main/issues/167[#167], https://github.com/CS2103-AY1819S2-W14-1/main/issues/153[#153], +https://github.com/CS2103-AY1819S2-W14-1/main/issues/160[#160], https://github.com/CS2103-AY1819S2-W14-1/main/issues/157[#157], + https://github.com/CS2103-AY1819S2-W14-1/main/issues/155[#155], https://github.com/CS2103-AY1819S2-W14-1/main/issues/148[#148]) + +** Tools: +*** Set up Travis CI for the team repo. +*** Set up Netlify for the team repo. +*** Set up Appveyor for the team repo. +*** Set up Codacy for the team repo. +*** Set up Coveralls for the team repo. + + +== Contributions to the User Guide + +--- + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +=== Filtering and Sorting Data + +include::../UserGuide.adoc[tag=fe] + +include::../UserGuide.adoc[tag=se] + +==== Listing all expenses : `list` + +include::../UserGuide.adoc[tag=list] + +==== Listing all expenses in reverse : `reverseList` + +include::../UserGuide.adoc[tag=reverse] + +==== Viewing help : `help` +Lists all the user commands with their syntax and descriptions. + +Format: `help` + +==== Listing the entered commands : `history` + +include::../UserGuide.adoc[tag=history] + +==== Share feature `[coming in v2.0]` + +Allows you to share your expenses with anyone, by sending them an email +including an attachment of your personal `ePiggy` data in CSV format. + + +== Contributions to the Developer Guide + +--- + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=findexpense] + +include::../DeveloperGuide.adoc[tag=sortexpense] + +include::../DeveloperGuide.adoc[tag=reverse] + + +== PROJECT: + +--- + +Check out my _GitHub_ profile [https://www.github.com/rahulb99[rahulb99]] . diff --git a/docs/team/tehwenyi.adoc b/docs/team/tehwenyi.adoc new file mode 100644 index 000000000000..978fc47d1ce4 --- /dev/null +++ b/docs/team/tehwenyi.adoc @@ -0,0 +1,186 @@ +:site-section: AboutUs +:imagesDir: ../images +:sectnums: +:stylesDir: ../stylesheets + +image::projectportfolioheader.png[width="550"] + +This portfolio aims to document my contributions to *_ePiggy_*. + +*_ePiggy_* Github link: https://github.com/CS2103-AY1819S2-W17-4/main + +== Introduction + +image::about.png[width="130"] + +*ePiggy* is a desktop application designed to inculcate good spending habits in students +through allowing them to track their finances. It includes everything from tracking expenses, managing budgets +to setting goals. + + +It is written in Java. Interactions by users are done through the Command Line Interface (CLI) and it has a +Graphical User Interface (GUI) created with JavaFX. It can be used on both Windows and Mac. + + +image::projectteam.png[width="130"] + +*_ePiggy_* is a product of twin modules CS2101 and CS2103T. It was transformed from the +link:https://github.com/nus-cs2103-AY1819S2/addressbook-level4[AddressBook - Level 4] application. +My team consists of 5 members, including myself. + + +== Summary of contributions +|=== +|_This section summarises my contributions to ePiggy's code and showcases my technical abilities._ +|=== + +=== Major Enhancement + +I added the `budget` feature of ePiggy, which consists of 3 commands: `addBudget`, `editBudget` and `deleteBudget`. + + +image::whatitdoes.png[width="150"] + +* The `budget` feature allows users to set a limit on their spendings for a specified time period and displays to users +the status of their spendings in relation to their set budget(s). +* The budgets set by the user are viewed as a list. + + +image::justification.png[width="150"] + +* The `budget` feature urges users to be more aware of the amount they spend per time period so that they can avoid +splurging. + +* Also, unlike typical expense trackers with only one budget, ePiggy's `budget` feature consists of a list of +budgets so that users can see their past spending habits and plan for future budgets. + +image::highlights.png[width="150"] + +* This enhancement was built from scratch. It is completely new and hence required the creation of many new objects +including `budget`, `period`, `uniqueBudgetList` and a new `budgetPanel` for the UI. This enhancement required me to be +familiar with almost all the different aspects of the code (`commons`, `logic`, `model`, `ui`). + +=== Minor Enhancement +Added the status and reminder components of the `budget` feature. + +image::whatitdoes.png[width="150"] + +* The `budget` feature will display the status of the budget (remaining amount, remaining days) and provide reminders +when users are going to or have spent more than the limit. + + +image::justification.png[width="150"] + +* *_ePiggy_*'s objective is to inculcate good spending habits in our users. Hence, users need to be constantly aware of +their spendings, especially in relation to the budget they have set. + +image::highlights.png[width="150"] + +* For this enhancement, the UI is important as it should be simple yet contain enough information. It was challenging to +make the UI simple and concise for our target users whilst conveying the crucial information. + +=== Code Contributed +As of 15th April 2019, I have contributed over 60 Pull Requests. + +* *Main functional codes:* link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/AddBudgetCommand.java[Add Budget] +| link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/EditBudgetCommand.java[Edit Budget] +| link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/logic/commands/epiggy/DeleteBudgetCommand.java[Delete Budget] +| link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/BudgetPanel.java[Budget Panel] +| link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/BudgetCard.java[Budget Card] + +* *Main test codes:* link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/AddBudgetCommandTest.java[Add Budget] +| link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/EditBudgetCommandTest.java[Edit Budget Budget] +| link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/DeleteBudgetCommandTest.java[Delete Budget] +| link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/systemtests/epiggy/AddBudgetCommandSystemTest.java[Budget System Test] + +View all the code I contributed link:https://nus-cs2103-ay1819s2.github.io/cs2103-dashboard/#search=tehwenyi&sort=displayName&since=2019-02-10&until=2019-04-15&timeframe=day&reverse=false&repoSort=true[here]! + +=== Other Contributions + +* Managed releases v1.3 and v1.4. +* As of *15th April 2019*: +** Reviewed over 25 pull requests on ePiggy's Github. +** Raised over 10 issues on ePiggy's Github. +* Refactored Address Book to ePiggy. +* Set up Coveralls for the team. +* Identified bugs in ePiggy and raised them as issues (https://github.com/CS2103-AY1819S2-W17-4/main/issues/109[#109], https://github.com/CS2103-AY1819S2-W17-4/main/issues/167[#167], https://github.com/CS2103-AY1819S2-W17-4/main/issues/242[#242]). +* Standardised the language and formatting of the user and developer guide to provide greater coherence. + +== Contributions to the User Guide + +|=== +|_Documented below are my contributions to the User Guide. They showcase my ability to convey clear and coherent +information for end-users who may not be familiar with computing technicalities._ +|=== + +Click link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/docs/UserGuide.adoc[here] to view the full User Guide! + +image::overview.png[width="130"] + +* *Standardised and improved the aesthetics of the User Guide* + +I designed and added the header and images across the user guide to make it more +inviting and reader friendly, considering that our target users are students. Below are some examples of my designs: + +.Header and section headers added in the User Guide to increase readability and better engage our target audience +image::userguideheaders.png[width="500"] + +* *Contributed content to the User Guide* + +I wrote the *Introduction* of the User Guide, making sure it is clear, concise and engaging. +Also, I wrote the *Budgeting* feature which teaches users how to use the `budget` commands in a clear and informative +manner. My contributions for the *Budgeting* feature can be found below, in Sections 3.1 to 3.3. + +image::extract.png[width="130"] + +|=== +|_My contribution towards the *Budgeting* feature in the User Guide can be split into 3 segments: `addBudget`, +`editBudget` and `deleteBudget`. The extracts in Sections 3.1 to 3.3 below are my contributions to the User Guide._ +|=== + +=== Add Budget Feature: addBudget + +include::../UserGuide.adoc[tag=addbudget] + +=== Edit Budget Feature: editBudget + +include::../UserGuide.adoc[tag=editbudget] + +=== Delete Budget Feature: deleteBudget + +include::../UserGuide.adoc[tag=deletebudget] + +== Contributions to the Developer Guide + +|=== +|_Documented below are my contributions to the Developer Guide. They showcase the technical depth of my contributions +to the project and my ability to convey all necessary technical information clearly._ +|=== + +Click link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/docs/DeveloperGuide.adoc[here] to view the full +Developer Guide! + +image::overview.png[width="130"] + +* *Standardised and improved the aesthetics of the Developer Guide* + +I designed and added the header and images across the developer guide to +reduce clutter of words and make it more readable. Below are some examples of my designs: + +.Header and images added in the Developer Guide. +image::developerguideheaders.png[width="500"] + +* *Contributed content to the Developer Guide* + +I documented the implementation details and design considerations for the *Budgeting* feature. +I also created diagrams to aid in the explanations. My contributions can be found below, in Sections 4.1 to 4.3. + +image::extract.png[width="130"] + +|=== +|_My contribution towards the Developer Guide can be split into 3 segments: `addBudget`, +`editBudget` and `deleteBudget`. The extracts in Sections 4.1 to 4.3 below are my contributions to the Developer Guide._ +|=== + +=== Add Budget Feature: addBudget + +include::../DeveloperGuide.adoc[tag=addbudget] + +=== Edit Budget Feature: editBudget + +include::../DeveloperGuide.adoc[tag=editbudget] + +=== Delete Budget Feature: deleteBudget + +include::../DeveloperGuide.adoc[tag=deletebudget] diff --git a/docs/team/yunjun199321.adoc b/docs/team/yunjun199321.adoc new file mode 100644 index 000000000000..72ee1d5b3f85 --- /dev/null +++ b/docs/team/yunjun199321.adoc @@ -0,0 +1,111 @@ += Wu Yunjun - Project Portfolio for ePiggy +:site-section: AboutUs +:imagesDir: ../images +:sectnums: +:stylesDir: ../stylesheets + +image::ppplogo.png[width="300"] +This portfolio aims to document my contributions to *_ePiggy_*. +Github link: https://github.com/CS2103-AY1819S2-W17-4/main + +--- +== About the project + +My team and I designed to modify a basic command line interface addressbook4 for our Software Engineering project. +We chose to change some logic and methods in addressbook4 so that it became a useful accounting software calls ePiggy. +The ePiggy aids to inculcate good spending habits in students. At the same time, ePiggy supports budgets, expenses and allowances management. +Student can use it to manage money easily by single and simply commands. + +My role was to design and write the codes for the `*report*` and `*auto-complete*` features. The following sections +illustrate these enhancements in more detail, as well as the relevant sections I have added to the +user and developer guides in relation to these enhancements. + +Note the following symbols and formatting used in this document: + +[NOTE] +This symbol indicates important information. + +`report` | A grey highlight (called a mark-up) indicates that this is a command + that can be inputted into the command line and executed by the + application. + +link:https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ReportWindow.java[`ReportWindow`] +| Underline text with grey highlight indicates a component, class or object in + the architecture of the application. + +== Summary of contributions + +This section shows a summary of my coding, documentation, and other helpful contributions to the +team project. + +=== *Major enhancements* + +* I added a `report` command for ePiggy: + +** What it does: The `report` command allows the user to view a report (summary) of expenses, budgets and allowance on specified date, month or year. +** Justification: If user wants to manage his/her money better, the report command enables ePiggy to generate a visualized report of specified date, month or year with different charts. The user can have a better idea of how much he/she spent or how much he/she save in that period of time. +** Highlights: +*** This command works with existing advanced features such as budget and allowance management. The implementations were challenging as it required good understanding of the addressbook4 structure and JavaFX. +*** This enhancement was built from scratch. I found my way to create a new window to show different charts and data. I tried multiply JavaFX panes to make sure that the charts and message nicely fit and display on the new window. +** Credits: JavaFX basic feature online tutorials, JavaFX advanced features (charts) tutorials. + +=== *Minor enhancements* + +* I added auto-complete feature for ePiggy + +** What it does: User enters first few letters of the command, and then press ‘Tab’ key on the keyboard. The completed command will show on the text field. +** Justification: In case the user forgot some of the commands, he/she can quickly find the correct command format by press the ‘Tab’ key. If user forgot the parameters of the command, he/she can also use it function fill in the correct parameters of the command. + +* *Code contributed*: Please click these links to see a sample of my code: + +** Functional code: +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ReportWindow.java[report feature]| +https://github.com/yunjun199321/main/commit/fcc384f89b82596b30448aa47c6f4b64151e67c7[Auto-complete function]| +https://github.com/yunjun199321/main/commit/17e48dfc8f152ba857fbe41ac729d03f20c493be[Report command parser and UI button]| +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/main/java/seedu/address/ui/ReportWindow.java[JavaFX charts] +** Test code: +https://github.com/CS2103-AY1819S2-W17-4/main/pull/223/commits/8cb923ff73b91af9e771ab99fa69a321e44a5f12[Two ePiggy System test super class]| +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/systemtests/epiggy/ReportCommandSystemTest.java[report command system test]| +https://github.com/CS2103-AY1819S2-W17-4/main/pull/228/commits/9157714093bc5a93524990aa00214526ed16567a[Help command System test]| +https://github.com/CS2103-AY1819S2-W17-4/main/pull/228/commits/84c02d96acd964a1b8104e558345ffc105176ecb[Clear command System test]| +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/parser/epiggy/ReportCommandParserTest.java[Report command parser test]| +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/UndoCommandTest.java[Undo command unit test]| +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/logic/commands/epiggy/RedoCommandTest.java[Redo command unit test]| +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/seedu/address/ui/CommandBoxTest.java[auto-complete unit test (handleKeyPress_tab)]| +https://github.com/CS2103-AY1819S2-W17-4/main/blob/master/src/test/java/systemtests/epiggy/ReportCommandIntegrationTest.java[Report command integration test] + +** All my contributions can be viewed https://nus-cs2103-ay1819s2.github.io/cs2103-dashboard/#=undefined&search=yunjun199321[RepoSense] + +=== Other contributions: + +** Project management: +*** Managed releases version 1.1 to 1.4 on GitHub +** Enhancements to existing features: +*** Updated the ePiggy icon and name https://github.com/CS2103-AY1819S2-W17-4/main/pull/35[(#35)] +*** Wrote additional tests for existing features to increase coverage https://github.com/CS2103-AY1819S2-W17-4/main/issues/159[(#159)] https://github.com/CS2103-AY1819S2-W17-4/main/pull/223[(#223)] +*** Fixed some old tests to increase coverage +** Documentation: +*** Made cosmetic improvements to the existing User Guide to make it more reader-friendly +*** Updated User Guide and Developer Guide https://github.com/CS2103-AY1819S2-W17-4/main/pull/229[(#229)] https://github.com/CS2103-AY1819S2-W17-4/main/pull/139[(#139)] +*** Added user stories +** Community: +*** PRs reviewed https://github.com/CS2103-AY1819S2-W17-4/main/pull/44[(#44)] https://github.com/CS2103-AY1819S2-W17-4/main/pull/39[(#39)] https://github.com/CS2103-AY1819S2-W17-4/main/pull/224[(#224)] +*** Contributed to forum discussions https://github.com/CS2103-AY1819S2-W17-4/main/pull/227[(#227)] https://github.com/CS2103-AY1819S2-W17-4/main/issues/195[(#195)] https://github.com/CS2103-AY1819S2-W17-4/main/issues/205[(#205)] +*** Reported bugs and suggestions for other teams in the class https://github.com/cs2103-ay1819S2-w11-2/main/issues/181[(#181)] https://github.com/cs2103-ay1819S2-w11-2/main/issues/173[(#173)] +** Tools: +*** Setup GitHub home-page and issue template +*** Setup Travis CI + +== Contributions to the User Guide + +We had to update the original addressbook4 User Guide with instructions for the enhancements that +we had added. The following is an excerpt from our ePiggy User Guide, showing the details of report features and auto complete function. + +include::../UserGuide.adoc[tag=report] + +include::../UserGuide.adoc[tag=autocomplete] + +== Contributions to the Developer Guide +The following section shows my additions to the ePiggy Developer Guide for the report and auto complete features. + +include::../DeveloperGuide.adoc[tag=report] + +include::../DeveloperGuide.adoc[tag=autocomplete] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2d80b69a7665..dc1842eee89c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Tue Mar 12 15:51:22 SGT 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index a92d4d5d71f0..34296112c262 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -15,19 +15,20 @@ 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.EPiggy; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyEPiggy; 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.model.epiggy.SampleEPiggyDataUtil; import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; import seedu.address.storage.UserPrefsStorage; +import seedu.address.storage.epiggy.EPiggyStorage; +import seedu.address.storage.epiggy.JsonEPiggyStorage; import seedu.address.ui.Ui; import seedu.address.ui.UiManager; @@ -36,7 +37,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 6, 0, true); + public static final Version VERSION = new Version(1, 4, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -46,9 +47,10 @@ public class MainApp extends Application { protected Model model; protected Config config; + @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing EPiggy ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -56,8 +58,9 @@ public void init() throws Exception { UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); UserPrefs userPrefs = initPrefs(userPrefsStorage); - AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); + + EPiggyStorage ePiggyStorage = new JsonEPiggyStorage(userPrefs.getEPiggyFilePath()); + storage = new StorageManager(ePiggyStorage, userPrefsStorage); initLogging(config); @@ -74,20 +77,20 @@ public void init() throws Exception { * 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) { - Optional addressBookOptional; - ReadOnlyAddressBook initialData; + Optional ePiggyOptional; + ReadOnlyEPiggy initialData; try { - addressBookOptional = storage.readAddressBook(); - if (!addressBookOptional.isPresent()) { - logger.info("Data file not found. Will be starting with a sample AddressBook"); + ePiggyOptional = storage.readEPiggy(); + if (!ePiggyOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a sample EPiggy"); } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + initialData = ePiggyOptional.orElseGet(SampleEPiggyDataUtil::getSampleEPiggy); } catch (DataConversionException e) { - logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Data file not in the correct format. Will be starting with an empty EPiggy"); + initialData = new EPiggy(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Problem while reading from the file. Will be starting with an empty EPiggy"); + initialData = new EPiggy(); } return new ModelManager(initialData, userPrefs); @@ -151,7 +154,7 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { + "Using default user prefs"); initializedPrefs = new UserPrefs(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); + logger.warning("Problem while reading from the file. Will be starting with an empty EPiggy"); initializedPrefs = new UserPrefs(); } @@ -167,13 +170,13 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting EPiggy " + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping ePiggy ] ============================="); try { storage.saveUserPrefs(model.getUserPrefs()); } catch (IOException e) { diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/seedu/address/commons/core/GuiSettings.java index 5ace559ad156..7e5257866e77 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/seedu/address/commons/core/GuiSettings.java @@ -10,8 +10,8 @@ */ public class GuiSettings implements Serializable { - private static final double DEFAULT_HEIGHT = 600; - private static final double DEFAULT_WIDTH = 740; + private static final double DEFAULT_HEIGHT = 700; + private static final double DEFAULT_WIDTH = 1000; private final double windowWidth; private final double windowHeight; diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e4695..e100810013ac 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -5,9 +5,18 @@ */ public class Messages { + public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; + public static final String MESSAGE_INVALID_EXPENSE_DISPLAYED_INDEX = "The index provided is invalid"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_EXPENSES_LISTED_OVERVIEW = "%1$d expenses listed!"; + + public static final String MESSAGE_INVALID_BUDGET_DISPLAYED_INDEX = "The budget index provided is invalid."; + public static final String MESSAGE_INVALID_DATE = "Date is invalid. Date format should be dd/mm/yyyy and date " + + "should be valid."; + public static final String MESSAGE_CANNOT_CREATE_ALLOWANCE_TAG = "The tag name 'Allowance' is not allowed."; + public static final String MESSAGE_CANNOT_CREATE_EXPENSE_TAG = "The tag name 'Expense' is not allowed."; } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 60369e2074e4..32ce9add5b01 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -3,12 +3,18 @@ import java.nio.file.Path; import javafx.beans.property.ReadOnlyProperty; +import javafx.beans.value.ObservableValue; 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.ReadOnlyEPiggy; +import seedu.address.model.epiggy.Budget; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.Goal; + +import seedu.address.model.epiggy.item.Cost; import seedu.address.model.person.Person; /** @@ -25,15 +31,24 @@ public interface Logic { CommandResult execute(String commandText) throws CommandException, ParseException; /** - * Returns the AddressBook. + * Returns the EPiggy. * - * @see seedu.address.model.Model#getAddressBook() + * @see seedu.address.model.Model#getEPiggy() */ - ReadOnlyAddressBook getAddressBook(); + ReadOnlyEPiggy getEPiggy(); /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of persons */ + ObservableList getFilteredExpenseList(); + + ObservableList getFilteredBudgetList(); + + ObservableValue getSavings(); + + ObservableValue getGoal(); + /** * Returns an unmodifiable view of the list of commands entered by the user. * The list is ordered from the least recent command to the most recent command. @@ -43,7 +58,7 @@ public interface Logic { /** * Returns the user prefs' address book file path. */ - Path getAddressBookFilePath(); + Path getEPiggyFilePath(); /** * Returns the user prefs' GUI settings. @@ -63,10 +78,39 @@ public interface Logic { */ ReadOnlyProperty selectedPersonProperty(); + /** + * Selected expense in the filtered expense list. + * null if no expense is selected. + * + * @see seedu.address.model.Model#selectedExpenseProperty() + */ + ReadOnlyProperty selectedExpenseProperty(); + /** * Sets the selected person in the filtered person list. * * @see seedu.address.model.Model#setSelectedPerson(Person) */ void setSelectedPerson(Person person); + + /** + * Sets the current budget. + * + * @see seedu.address.model.Model#addBudget(Budget) + */ + void setCurrentBudget(Budget budget); + + /** + * Adds a new budget. + * + * @see seedu.address.model.Model#addBudget(Budget) + */ + void addBudget(int index, Budget budget); + + /** + * Sets the selected expense in the filtered person list. + * + * @see seedu.address.model.Model#setSelectedExpense(Expense) + */ + void setSelectedExpense(Expense expense); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5cb24a617beb..7e67e6e5c655 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -5,16 +5,22 @@ import java.util.logging.Logger; import javafx.beans.property.ReadOnlyProperty; +import javafx.beans.value.ObservableValue; 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.EPiggyParser; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyEPiggy; +import seedu.address.model.epiggy.Budget; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.Goal; + +import seedu.address.model.epiggy.item.Cost; import seedu.address.model.person.Person; import seedu.address.storage.Storage; @@ -28,17 +34,17 @@ public class LogicManager implements Logic { private final Model model; private final Storage storage; private final CommandHistory history; - private final AddressBookParser addressBookParser; + private final EPiggyParser ePiggyParser; private boolean addressBookModified; public LogicManager(Model model, Storage storage) { this.model = model; this.storage = storage; history = new CommandHistory(); - addressBookParser = new AddressBookParser(); + ePiggyParser = new EPiggyParser(); // Set addressBookModified to true whenever the models' address book is modified. - model.getAddressBook().addListener(observable -> addressBookModified = true); + model.getEPiggy().addListener(observable -> addressBookModified = true); } @Override @@ -48,16 +54,18 @@ public CommandResult execute(String commandText) throws CommandException, ParseE CommandResult commandResult; try { - Command command = addressBookParser.parseCommand(commandText); + Command command = ePiggyParser.parseCommand(commandText); commandResult = command.execute(model, history); } finally { - history.add(commandText); + if (!commandText.isEmpty()) { + history.add(commandText); + } } if (addressBookModified) { - logger.info("Address book modified, saving to file."); + logger.info("ePiggy modified, saving to file."); try { - storage.saveAddressBook(model.getAddressBook()); + storage.saveEPiggy(model.getEPiggy()); } catch (IOException ioe) { throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); } @@ -67,8 +75,8 @@ public CommandResult execute(String commandText) throws CommandException, ParseE } @Override - public ReadOnlyAddressBook getAddressBook() { - return model.getAddressBook(); + public ReadOnlyEPiggy getEPiggy() { + return model.getEPiggy(); } @Override @@ -76,14 +84,35 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getFilteredExpenseList() { + return model.getFilteredExpenseList(); + } + + //@@author tehwenyi + @Override + public ObservableList getFilteredBudgetList() { + return model.getFilteredBudgetList(); + } + + @Override + public ObservableValue getSavings() { + return model.getSavings(); + } + + @Override + public ObservableValue getGoal() { + return model.getGoal(); + } + @Override public ObservableList getHistory() { return history.getHistory(); } @Override - public Path getAddressBookFilePath() { - return model.getAddressBookFilePath(); + public Path getEPiggyFilePath() { + return model.getEPiggyFilePath(); } @Override @@ -105,4 +134,24 @@ public ReadOnlyProperty selectedPersonProperty() { public void setSelectedPerson(Person person) { model.setSelectedPerson(person); } + + @Override + public void setSelectedExpense(Expense expense) { + model.setSelectedExpense(expense); + } + + //@@author tehwenyi + @Override + public void setCurrentBudget(Budget budget) { + model.setCurrentBudget(budget); } + + //@@author tehwenyi + @Override + public void addBudget(int index, Budget budget) { + model.addBudget(index, budget); } + + @Override + public ReadOnlyProperty selectedExpenseProperty() { + return model.selectedExpenseProperty(); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index d88e831ff1ce..5654d12ecac7 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -2,6 +2,9 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; 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; @@ -12,30 +15,35 @@ import seedu.address.model.Model; import seedu.address.model.person.Person; +//import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +//import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; + /** * Adds a person to the address book. */ public class AddCommand extends Command { - public static final String COMMAND_WORD = "add"; + public static final String COMMAND_ALIAS = "a"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds an expense to the ePiggy. " + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_NAME + "EXPENSE NAME " + + PREFIX_COST + "COST " + + PREFIX_CATEGORY + "CATEGORY " + + "[" + PREFIX_DATE + "DATE] " + + "[" + PREFIX_ADDRESS + "ADDRESS] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " + + PREFIX_PHONE + "9820304 " + + PREFIX_EMAIL + "123@nus.edu.sg " + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_TAG + "Lunch"; - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + public static final String MESSAGE_SUCCESS = "New expense added: %1$s"; + // public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + public static final String MESSAGE_DUPLICATE_EXPENSE = "This expense already exists int he address book"; private final Person toAdd; @@ -52,11 +60,12 @@ public CommandResult execute(Model model, CommandHistory history) throws Command requireNonNull(model); if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + //throw new CommandException(MESSAGE_DUPLICATE_PERSON); + throw new CommandException(MESSAGE_DUPLICATE_EXPENSE); } model.addPerson(toAdd); - model.commitAddressBook(); + model.commitEPiggy(); return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); } diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index a22219ad76ad..d9c48a2fb2ca 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -3,7 +3,7 @@ import static java.util.Objects.requireNonNull; import seedu.address.logic.CommandHistory; -import seedu.address.model.AddressBook; +import seedu.address.model.EPiggy; import seedu.address.model.Model; /** @@ -12,14 +12,14 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - + public static final String COMMAND_ALIAS = "c"; + public static final String MESSAGE_SUCCESS = "ePiggy has been cleared!"; @Override public CommandResult execute(Model model, CommandHistory history) { requireNonNull(model); - model.setAddressBook(new AddressBook()); - model.commitAddressBook(); + model.setEPiggy(new EPiggy()); + model.commitEPiggy(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 92f900b7916d..6cc0664df662 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -11,12 +11,21 @@ public class CommandResult { private final String feedbackToUser; - /** Help information should be shown to the user. */ + /** + * Help information should be shown to the user. + */ private final boolean showHelp; - /** The application should exit. */ + /** + * The application should exit. + */ private final boolean exit; + /** + * Summary should be shown to the user. + */ + private boolean showSummary; + /** * Constructs a {@code CommandResult} with the specified fields. */ @@ -26,6 +35,17 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { this.exit = exit; } + /** + * Constructs a {@code CommandResult} with the specified fields. + */ + //@@author yunjun199321 + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, boolean summary) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.showHelp = showHelp; + this.exit = exit; + this.showSummary = summary; + } + //@@author /** * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, * and other fields set to their default value. @@ -46,6 +66,10 @@ public boolean isExit() { return exit; } + public boolean isSummary() { + return showSummary; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -68,4 +92,13 @@ public int hashCode() { return Objects.hash(feedbackToUser, showHelp, exit); } + @Override + public String toString() { + return "CommandResult{" + + "feedbackToUser='" + feedbackToUser + '\'' + + ", showHelp=" + showHelp + + ", exit=" + exit + + ", showSummary=" + showSummary + + '}'; + } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index a20e9d49eac7..c9c0ebd9cebb 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -17,6 +17,7 @@ public class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; + public static final String COMMAND_ALIAS = "d"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes the person identified by the index number used in the displayed person list.\n" @@ -33,6 +34,7 @@ public DeleteCommand(Index targetIndex) { @Override public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); List lastShownList = model.getFilteredPersonList(); @@ -42,7 +44,7 @@ public CommandResult execute(Model model, CommandHistory history) throws Command Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); model.deletePerson(personToDelete); - model.commitAddressBook(); + model.commitEPiggy(); return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 952a9e7e7f2b..aaf4ca533f9a 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -33,6 +33,7 @@ public class EditCommand extends Command { public static final String COMMAND_WORD = "edit"; + public static final String COMMAND_ALIAS = "ed"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " + "by the index number used in the displayed person list. " @@ -84,7 +85,7 @@ public CommandResult execute(Model model, CommandHistory history) throws Command model.setPerson(personToEdit, editedPerson); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - model.commitAddressBook(); + model.commitEPiggy(); return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); } diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 2240a3e4be1f..d2957db255a4 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -9,8 +9,9 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; + public static final String COMMAND_ALIAS = "ex"; - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting ePiggy as requested ..."; @Override public CommandResult execute(Model model, CommandHistory history) { diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index beb178e3a3f5..2a8827253b6c 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -14,6 +14,7 @@ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; + public static final String COMMAND_ALIAS = "f"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index f0ef78dddded..0882b12ba9c5 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -9,12 +9,19 @@ public class HelpCommand extends Command { public static final String COMMAND_WORD = "help"; + public static final String COMMAND_ALIAS = "hp"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows ePiggy's usage instructions.\n" + "Example: " + COMMAND_WORD; - public static final String SHOWING_HELP_MESSAGE = "Opened help window."; + public static final String SHOWING_HELP_MESSAGE = "Opened help window!"; + /** + * Execute `help` command. + * @param model {@code Model} which the command should operate on. + * @param history {@code CommandHistory} which the command should operate on. + * @return + */ @Override public CommandResult execute(Model model, CommandHistory history) { return new CommandResult(SHOWING_HELP_MESSAGE, true, false); diff --git a/src/main/java/seedu/address/logic/commands/HistoryCommand.java b/src/main/java/seedu/address/logic/commands/HistoryCommand.java index dc3de1aad55e..29d2338e1db1 100644 --- a/src/main/java/seedu/address/logic/commands/HistoryCommand.java +++ b/src/main/java/seedu/address/logic/commands/HistoryCommand.java @@ -14,6 +14,7 @@ public class HistoryCommand extends Command { public static final String COMMAND_WORD = "history"; + public static final String COMMAND_ALIAS = "hs"; public static final String MESSAGE_SUCCESS = "Entered commands (from most recent to earliest):\n%1$s"; public static final String MESSAGE_NO_HISTORY = "You have not yet entered any commands."; diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 6d44824c7d1b..96bcfc989209 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -1,25 +1,27 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; import seedu.address.logic.CommandHistory; import seedu.address.model.Model; +//@@author rahulb99 /** * 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 COMMAND_ALIAS = "l"; - public static final String MESSAGE_SUCCESS = "Listed all persons"; + public static final String MESSAGE_SUCCESS = "Listed all expenses."; @Override public CommandResult execute(Model model, CommandHistory history) { requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java index 227771a4eef6..85bd35c303c6 100644 --- a/src/main/java/seedu/address/logic/commands/RedoCommand.java +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -13,6 +13,7 @@ public class RedoCommand extends Command { public static final String COMMAND_WORD = "redo"; + public static final String COMMAND_ALIAS = "r"; public static final String MESSAGE_SUCCESS = "Redo success!"; public static final String MESSAGE_FAILURE = "No more commands to redo!"; @@ -20,11 +21,11 @@ public class RedoCommand extends Command { public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); - if (!model.canRedoAddressBook()) { + if (!model.canRedoEPiggy()) { throw new CommandException(MESSAGE_FAILURE); } - model.redoAddressBook(); + model.redoEPiggy(); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); return new CommandResult(MESSAGE_SUCCESS); } diff --git a/src/main/java/seedu/address/logic/commands/SelectCommand.java b/src/main/java/seedu/address/logic/commands/SelectCommand.java index baa3c1f30bb4..16c027eaaa13 100644 --- a/src/main/java/seedu/address/logic/commands/SelectCommand.java +++ b/src/main/java/seedu/address/logic/commands/SelectCommand.java @@ -9,7 +9,7 @@ import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; -import seedu.address.model.person.Person; +import seedu.address.model.epiggy.Expense; /** * Selects a person identified using it's displayed index from the address book. @@ -17,6 +17,7 @@ public class SelectCommand extends Command { public static final String COMMAND_WORD = "select"; + public static final String COMMAND_ALIAS = "s"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Selects the person identified by the index number used in the displayed person list.\n" @@ -35,13 +36,13 @@ public SelectCommand(Index targetIndex) { public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); - List filteredPersonList = model.getFilteredPersonList(); + List filteredExpenseList = model.getFilteredExpenseList(); - if (targetIndex.getZeroBased() >= filteredPersonList.size()) { + if (targetIndex.getZeroBased() >= filteredExpenseList.size()) { throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } - model.setSelectedPerson(filteredPersonList.get(targetIndex.getZeroBased())); + model.setSelectedExpense(filteredExpenseList.get(targetIndex.getZeroBased())); return new CommandResult(String.format(MESSAGE_SELECT_PERSON_SUCCESS, targetIndex.getOneBased())); } diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java index 40441264f346..55c56c77a642 100644 --- a/src/main/java/seedu/address/logic/commands/UndoCommand.java +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -13,18 +13,19 @@ public class UndoCommand extends Command { public static final String COMMAND_WORD = "undo"; + public static final String COMMAND_ALIAS = "u"; public static final String MESSAGE_SUCCESS = "Undo success!"; - public static final String MESSAGE_FAILURE = "No more commands to undo!"; + public static final String MESSAGE_FAILURE = "Can't undo!"; @Override public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); - if (!model.canUndoAddressBook()) { + if (!model.canUndoEPiggy()) { throw new CommandException(MESSAGE_FAILURE); } - model.undoAddressBook(); + model.undoEPiggy(); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); return new CommandResult(MESSAGE_SUCCESS); } diff --git a/src/main/java/seedu/address/logic/commands/epiggy/AddAllowanceCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/AddAllowanceCommand.java new file mode 100644 index 000000000000..530a60677f39 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/AddAllowanceCommand.java @@ -0,0 +1,64 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Allowance; + +//@@author kev-inc + +/** + * Adds an allowance to ePiggy. + */ +public class AddAllowanceCommand extends Command { + + public static final String COMMAND_WORD = "addAllowance"; + public static final String COMMAND_ALIAS = "aa"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an allowance to the epiggy. " + + "Parameters: " + + PREFIX_NAME + "ALLOWANCE NAME " + + PREFIX_COST + "AMOUNT " + + "[" + PREFIX_TAG + "TAG]..." + + "[" + PREFIX_DATE + "DATE] \n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "From Mummy " + + PREFIX_COST + "30 " + + PREFIX_TAG + "Weekly " + + PREFIX_DATE + "21/03/2019 "; + + public static final String MESSAGE_SUCCESS = "New allowance added.\nAdded allowance's details:\n%1$s"; + + private final Allowance toAdd; + + public AddAllowanceCommand(Allowance toAdd) { + requireNonNull(toAdd); + this.toAdd = toAdd; + } + + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + model.addAllowance(toAdd); + model.commitEPiggy(); + + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddAllowanceCommand // instanceof handles nulls + && toAdd.equals(((AddAllowanceCommand) other).toAdd)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/AddBudgetCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/AddBudgetCommand.java new file mode 100644 index 000000000000..753ae1ec82b9 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/AddBudgetCommand.java @@ -0,0 +1,108 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERIOD; +import static seedu.address.model.epiggy.UniqueBudgetList.MAXIMUM_SIZE; + +import java.util.Date; +import java.util.List; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Budget; + +//@@author tehwenyi + +/** + * Sets a budget for the total expenses. + */ +public class AddBudgetCommand extends Command { + public static final String COMMAND_WORD = "addBudget"; + public static final String COMMAND_ALIAS = "ab"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds a budget to ePiggy.\n" + + "Parameters: " + + PREFIX_COST + "BUDGET_IN_DOLLARS " + + PREFIX_PERIOD + "TIME_PERIOD_IN_DAYS " + + PREFIX_DATE + "START_DATE_IN_DD/MM/YYYY \n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_COST + "500 " + + PREFIX_PERIOD + "30 " + + PREFIX_DATE + "01/04/2019"; + + public static final String MESSAGE_SUCCESS = "Budget has been added.\nAdded budget's details:\n%1$s"; + public static final String MESSAGE_FAIL = "Budget is too old to be added." + + " The budget list can only accommodate a maximum of " + MAXIMUM_SIZE + + " budgets. \nIf you wish to add this budget," + + " please delete one of the existing budgets and add this budget again."; + public static final String MESSAGE_OVERLAPPING_BUDGET = "Budgets should not overlap.\n" + + "Please ensure that the time duration of the budget you have entered " + + "does not overlap with any existing budget."; + + private final Budget toAdd; + + /** + * Creates an AddBudgetCommand to add the specified {@code Budget} + */ + public AddBudgetCommand(Budget budget) { + requireNonNull(budget); + toAdd = budget; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + Date startDate = toAdd.getStartDate(); + Date endDate = toAdd.getEndDate(); + + List currentList = model.getFilteredBudgetList(); + if (currentList.size() == 0) { + model.addBudget(0, toAdd); + model.commitEPiggy(); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + + int index = 0; + Budget earlierBudget; + Budget laterBudget = currentList.get(0); + while (index < currentList.size()) { + earlierBudget = currentList.get(index); + if (index > 0) { + laterBudget = currentList.get(index - 1); + } + if (model.budgetsOverlap(startDate, endDate, earlierBudget)) { + throw new CommandException(MESSAGE_OVERLAPPING_BUDGET); + } + if (!startDate.before(earlierBudget.getEndDate())) { + if (index == 0 || !endDate.after(laterBudget.getStartDate())) { + model.addBudget(index, toAdd); + model.commitEPiggy(); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } else { + throw new CommandException(MESSAGE_OVERLAPPING_BUDGET); + } + } + index++; + } + if (index < MAXIMUM_SIZE) { + model.addBudget(index, toAdd); + model.commitEPiggy(); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + return new CommandResult(MESSAGE_FAIL); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddBudgetCommand // instanceof handles nulls + && toAdd.equals(((AddBudgetCommand) other).toAdd)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/AddExpenseCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/AddExpenseCommand.java new file mode 100644 index 000000000000..26ab7d8a7f9c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/AddExpenseCommand.java @@ -0,0 +1,51 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Expense; + +/** + * Adds an expense to the expense list. + */ +public class AddExpenseCommand extends Command { + + public static final String COMMAND_WORD = "addExpense"; + public static final String COMMAND_ALIAS = "ae"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an expense to the expense book.\n" + + "Parameters: " + + PREFIX_NAME + "EXPENSE NAME " + + PREFIX_COST + "COST " + + "[" + PREFIX_TAG + "TAG]..." + + "[" + PREFIX_DATE + "DATE] \n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "Chicken Rice " + + PREFIX_COST + "3.50 " + + PREFIX_TAG + "Lunch" + + PREFIX_DATE + "31/02/2019 "; + + public static final String MESSAGE_SUCCESS = "New expense added.\nAdded expense's details:\n%1$s"; + + private final Expense toAdd; + + public AddExpenseCommand(Expense expense) { + this.toAdd = expense; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + model.addExpense(toAdd); + model.commitEPiggy(); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/DeleteAllowanceCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/DeleteAllowanceCommand.java new file mode 100644 index 000000000000..0759f940c6b3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/DeleteAllowanceCommand.java @@ -0,0 +1,68 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.Expense; + +//@@author kev-inc + +/** + * Deletes an allowance in epiggy. + */ +public class DeleteAllowanceCommand extends Command { + public static final String COMMAND_WORD = "deleteAllowance"; + public static final String COMMAND_ALIAS = "da"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the allowance identified by the index number used in the displayed expense list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_ALLOWANCE_SUCCESS = "Allowance deleted.\nDeleted allowance's details:" + + "\n%1$s"; + public static final String MESSAGE_INDEX_OUT_OF_BOUNDS = "The index does not exist on the list."; + public static final String MESSAGE_ITEM_NOT_ALLOWANCE = "The item selected is not an allowance.\n" + + "Please use " + DeleteExpenseCommand.COMMAND_WORD + " to delete expenses and " + + COMMAND_WORD + " to delete allowances."; + + + private final Index targetIndex; + + public DeleteAllowanceCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownExpenseList = model.getFilteredExpenseList(); + + if (targetIndex.getZeroBased() >= lastShownExpenseList.size()) { + throw new CommandException(MESSAGE_INDEX_OUT_OF_BOUNDS); + } + + Expense expenseToDelete = lastShownExpenseList.get(this.targetIndex.getZeroBased()); + if (!(expenseToDelete instanceof Allowance)) { + throw new CommandException(MESSAGE_ITEM_NOT_ALLOWANCE); + } + model.deleteExpense(expenseToDelete); + model.commitEPiggy(); + return new CommandResult(String.format(MESSAGE_DELETE_ALLOWANCE_SUCCESS, expenseToDelete)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteAllowanceCommand // instanceof handles nulls + && targetIndex.equals(((DeleteAllowanceCommand) other).targetIndex)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/DeleteBudgetCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/DeleteBudgetCommand.java new file mode 100644 index 000000000000..dfc0ab22dc77 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/DeleteBudgetCommand.java @@ -0,0 +1,60 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Budget; + +//@@author tehwenyi + +/** + * Deletes a budget from ePiggy. + */ +public class DeleteBudgetCommand extends Command { + + public static final String COMMAND_WORD = "deleteBudget"; + public static final String COMMAND_ALIAS = "db"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the budget identified by the index number used in the displayed budget list.\n" + + "Parameter: index (positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_BUDGET_SUCCESS = "Deleted budget. \nDeleted budget's details:\n%1$s"; + + private final Index targetIndex; + + public DeleteBudgetCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownBudgetList = model.getFilteredBudgetList(); + + if (targetIndex.getZeroBased() >= lastShownBudgetList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_BUDGET_DISPLAYED_INDEX); + } + + Budget budgetToDelete = lastShownBudgetList.get(this.targetIndex.getZeroBased()); + model.deleteBudgetAtIndex(this.targetIndex.getZeroBased()); + model.commitEPiggy(); + return new CommandResult(String.format(MESSAGE_DELETE_BUDGET_SUCCESS, budgetToDelete)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteBudgetCommand // instanceof handles nulls + && targetIndex.equals(((DeleteBudgetCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/DeleteExpenseCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/DeleteExpenseCommand.java new file mode 100644 index 000000000000..22b48fddcd64 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/DeleteExpenseCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.Expense; + +/** + * Delete an expense in ePiggy. + */ +public class DeleteExpenseCommand extends Command { + + public static final String COMMAND_WORD = "deleteExpense"; + public static final String COMMAND_ALIAS = "de"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the expense identified by the index used in the displayed expense list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_EXPENSE_SUCCESS = "Expense deleted.\nDeleted expense's details:\n%1$s"; + public static final String MESSAGE_INDEX_OUT_OF_BOUNDS = "The index does not exist on the expense list."; + public static final String MESSAGE_ITEM_NOT_EXPENSE = "The item selected is not an expense. " + + "Please use " + COMMAND_WORD + " to delete expenses and " + + DeleteAllowanceCommand.COMMAND_WORD + " to delete allowances."; + + private final Index targetIndex; + + public DeleteExpenseCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownExpenseList = model.getFilteredExpenseList(); + + if (targetIndex.getZeroBased() >= lastShownExpenseList.size()) { + throw new CommandException(MESSAGE_INDEX_OUT_OF_BOUNDS); + } + + Expense expenseToDelete = lastShownExpenseList.get(this.targetIndex.getZeroBased()); + if (expenseToDelete instanceof Allowance) { + throw new CommandException(MESSAGE_ITEM_NOT_EXPENSE); + } + model.deleteExpense(expenseToDelete); + model.commitEPiggy(); + return new CommandResult(String.format(MESSAGE_DELETE_EXPENSE_SUCCESS, expenseToDelete)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/EditAllowanceCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/EditAllowanceCommand.java new file mode 100644 index 000000000000..b7d8a6d006fd --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/EditAllowanceCommand.java @@ -0,0 +1,233 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; + +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Item; +import seedu.address.model.epiggy.item.Name; +import seedu.address.model.tag.Tag; + +//@@author kev-inc + +/** + * Edits the details of an existing allowance. + */ +public class EditAllowanceCommand extends Command { + + public static final String COMMAND_WORD = "editAllowance"; + public static final String COMMAND_ALIAS = "ea"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the allowance identified " + + "by the index in the displayed list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_COST + "COST] " + + "[" + PREFIX_DATE + "DATE] " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_COST + "5 "; + + public static final String MESSAGE_EDIT_ALLOWANCE_SUCCESS = "Allowance edited.\nEdited allowance's details:\n%1$s"; + public static final String MESSAGE_NOT_EDITED = "Allowance not edited as there are no changes.\n" + + MESSAGE_USAGE; + + public static final String MESSAGE_ITEM_NOT_ALLOWANCE = "The item selected is not an allowance. " + + "Please use " + EditExpenseCommand.COMMAND_WORD + " to edit expenses and " + + COMMAND_WORD + " to edit allowances."; + + final Index index; + final EditAllowanceDescriptor editAllowanceDescriptor; + + /** + * @param index of the allowance in the filtered expenses list to edit + * @param editAllowanceDescriptor details to edit the allowance with + */ + public EditAllowanceCommand(Index index, EditAllowanceDescriptor editAllowanceDescriptor) { + requireNonNull(index); + requireNonNull(editAllowanceDescriptor); + + this.index = index; + this.editAllowanceDescriptor = new EditAllowanceDescriptor(editAllowanceDescriptor); + + } + + @Override + public String toString() { + return editAllowanceDescriptor.toString(); + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredExpenseList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_DISPLAYED_INDEX); + } + + Expense toEdit = lastShownList.get(index.getZeroBased()); + if (!(toEdit instanceof Allowance)) { + throw new CommandException(MESSAGE_ITEM_NOT_ALLOWANCE); + } + Allowance editedAllowance = createEditedAllowance((Allowance) toEdit, editAllowanceDescriptor); + + model.setExpense(toEdit, editedAllowance); + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + model.commitEPiggy(); + return new CommandResult(String.format(MESSAGE_EDIT_ALLOWANCE_SUCCESS, editedAllowance)); + } + + /** + * Creates and returns a {@code Allowance} with the details of {@code allowanceToEdit} + * edited with {@code editAllowanceDescriptor}. + */ + static Allowance createEditedAllowance(Allowance allowanceToEdit, EditAllowanceDescriptor editAllowanceDescriptor) { + assert allowanceToEdit != null; + + Name updatedName = editAllowanceDescriptor.getName().orElse(allowanceToEdit.getItem().getName()); + Cost updatedCost = editAllowanceDescriptor.getCost().orElse(allowanceToEdit.getItem().getCost()); + Date updatedDate = editAllowanceDescriptor.getDate().orElse(allowanceToEdit.getDate()); + Set updatedTags = editAllowanceDescriptor.getTags().orElse(allowanceToEdit.getItem().getTags()); + return new Allowance(new Item(updatedName, updatedCost, updatedTags), updatedDate); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditAllowanceCommand)) { + return false; + } + + // state check + EditAllowanceCommand e = (EditAllowanceCommand) other; + return index.equals(e.index) + && editAllowanceDescriptor.equals(e.editAllowanceDescriptor); + } + /** + * Stores the details to edit the allowance with. Each non-empty field value will replace the + * corresponding field value of the allowance. + */ + public static class EditAllowanceDescriptor { + private Name name; + private Cost cost; + private Date date; + private Set tags; + + public EditAllowanceDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditAllowanceDescriptor(EditAllowanceDescriptor toCopy) { + setName(toCopy.name); + setCost(toCopy.cost); + setDate(toCopy.date); + setTags(toCopy.tags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, cost, date, tags); + } + + public void setName(seedu.address.model.epiggy.item.Name name) { + this.name = name; + } + + public void setCost(Cost cost) { + this.cost = cost; + } + + public void setDate(Date date) { + this.date = date; + } + + /** + * 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 Optional getName() { + return Optional.ofNullable(name); + } + + public Optional getCost() { + return Optional.ofNullable(cost); + } + + public Optional getDate() { + return Optional.ofNullable(date); + } + + /** + * 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() { + if (tags != null) { + tags.add(new Tag("Allowance")); + return Optional.of(Collections.unmodifiableSet(tags)); + } else { + return Optional.empty(); + } + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditAllowanceDescriptor)) { + return false; + } + + // state check + EditAllowanceDescriptor e = (EditAllowanceDescriptor) other; + + return getName().equals(e.getName()) + && getCost().equals(e.getCost()) + && getDate().equals(e.getDate()) + && getTags().equals(e.getTags()); + } + } + + +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/EditBudgetCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/EditBudgetCommand.java new file mode 100644 index 000000000000..f3517606ec94 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/EditBudgetCommand.java @@ -0,0 +1,200 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.commands.epiggy.AddBudgetCommand.MESSAGE_OVERLAPPING_BUDGET; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERIOD; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_BUDGETS; + +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Budget; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Period; + +//@@author tehwenyi + +/** + * Edits the current budget in ePiggy. + */ +public class EditBudgetCommand extends Command { + + public static final String COMMAND_WORD = "editBudget"; + public static final String COMMAND_ALIAS = "eb"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the current budget. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: [" + PREFIX_COST + "BUDGET_IN_DOLLARS] " + + "[" + PREFIX_PERIOD + "TIME_PERIOD_IN_DAYS] " + + "[" + PREFIX_DATE + "START_DATE_IN_DD/MM/YYYY]\n" + + "Example: " + COMMAND_WORD + + PREFIX_COST + "200 " + + PREFIX_PERIOD + "7"; + + public static final String MESSAGE_EDIT_BUDGET_SUCCESS = "Current budget updated.\nEdited budget's details:\n%1$s"; + public static final String MESSAGE_EDIT_BUDGET_DOES_NOT_EXIST_FAIL = "Only the current budget can be edited." + + " There is no current budget to be edited."; + public static final String MESSAGE_NOT_EDITED = "Budget not edited as there are no changes.\n" + + MESSAGE_USAGE; + + private final EditBudgetDetails editBudgetDetails; + + public EditBudgetCommand(EditBudgetDetails editBudgetDetails) { + requireNonNull(editBudgetDetails); + this.editBudgetDetails = editBudgetDetails; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownBudgetList = model.getFilteredBudgetList(); + + int indexOfCurrentBudget = model.getCurrentBudgetIndex(); + if (indexOfCurrentBudget == -1) { + throw new CommandException(MESSAGE_EDIT_BUDGET_DOES_NOT_EXIST_FAIL); + } + Budget budgetToEdit = lastShownBudgetList.get(indexOfCurrentBudget); + Budget editedBudget = createEditedBudget(budgetToEdit, editBudgetDetails); + + if (lastShownBudgetList.size() > 1) { + + // If the budget is not the latest budget + if (indexOfCurrentBudget > 0) { + Budget laterBudget = lastShownBudgetList.get(indexOfCurrentBudget - 1); + if (editedBudget.getEndDate().after(laterBudget.getStartDate())) { + throw new CommandException(MESSAGE_OVERLAPPING_BUDGET); + } + } + // If the budget is not the earliest budget + if (indexOfCurrentBudget < lastShownBudgetList.size() - 1) { + Budget earlierBudget = lastShownBudgetList.get(indexOfCurrentBudget + 1); + if (editedBudget.getStartDate().before(earlierBudget.getEndDate())) { + throw new CommandException(MESSAGE_OVERLAPPING_BUDGET); + } + } + } + + model.setCurrentBudget(editedBudget); + model.updateFilteredBudgetList(PREDICATE_SHOW_ALL_BUDGETS); + model.commitEPiggy(); + return new CommandResult(String.format(MESSAGE_EDIT_BUDGET_SUCCESS, editedBudget)); + } + + /** + * Creates and returns a {@code Budget} with the details of {@code budgetToEdit} + * edited with {@code editBudgetDetails}. + */ + public static Budget createEditedBudget(Budget budgetToEdit, EditBudgetDetails editBudgetDetails) { + assert budgetToEdit != null; + + Cost updatedAmount = editBudgetDetails.getAmount().orElse(budgetToEdit.getBudgetedAmount()); + Date updatedStartDate = editBudgetDetails.getStartDate().orElse((budgetToEdit.getStartDate())); + Period updatedPeriod = editBudgetDetails.getPeriod().orElse(budgetToEdit.getPeriod()); + + return new Budget(updatedAmount, updatedPeriod, updatedStartDate); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditBudgetCommand)) { + return false; + } + + // state check + EditBudgetCommand e = (EditBudgetCommand) other; + return this.editBudgetDetails.equals(e.editBudgetDetails); + } + + /** + * Stores the details to edit the budget with. Each non-empty field value will replace the + * corresponding field value of the budget. + */ + public static class EditBudgetDetails { + private Cost amount; + private Date startDate; + private Period period; + + public EditBudgetDetails() {} + + /** + * Copy constructor. + */ + public EditBudgetDetails(EditBudgetDetails toCopy) { + setAmount(toCopy.amount); + setStartDate(toCopy.startDate); + setPeriod(toCopy.period); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(amount, startDate, period); + } + + public void setAmount(Cost amount) { + this.amount = amount; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public void setPeriod(Period period) { + this.period = period; + } + + public Optional getAmount() { + return Optional.ofNullable(amount); + } + + public Optional getStartDate() { + return Optional.ofNullable(startDate); + } + + public Optional getPeriod() { + return Optional.ofNullable(period); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditBudgetDetails)) { + return false; + } + + // state check + EditBudgetDetails e = (EditBudgetDetails) other; + + return getAmount().equals(e.getAmount()) + && getStartDate().equals(e.getStartDate()) + && getPeriod().equals(e.getPeriod()); + } + + @Override + public String toString() { + return new String("Amount of $" + amount + " and period of " + period + + " starting from " + startDate); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/EditExpenseCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/EditExpenseCommand.java new file mode 100644 index 000000000000..62aa32f0517c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/EditExpenseCommand.java @@ -0,0 +1,225 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; + +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Item; +import seedu.address.model.epiggy.item.Name; +import seedu.address.model.tag.Tag; + +/** + * Edits the details of an existing expense. + */ +public class EditExpenseCommand extends Command { + + public static final String COMMAND_WORD = "editExpense"; + public static final String COMMAND_ALIAS = "ee"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the expense identified " + + "by the index in the displayed expense list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_COST + "COST] " + + "[" + PREFIX_DATE + "DATE] " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_COST + "5 " + + PREFIX_TAG + "food"; + + public static final String MESSAGE_EDIT_EXPENSE_SUCCESS = "Expense edited.\nEdited expense's details:\n%1$s"; + public static final String MESSAGE_NOT_EDITED = "Expense not edited as there are no changes."; + + private static final String MESSAGE_ITEM_NOT_EXPENSE = "The item selected is not an expense. " + + "Please use " + COMMAND_WORD + " to edit expenses and " + + EditAllowanceCommand.COMMAND_WORD + " to edit allowances."; + + final Index index; + final EditExpenseDescriptor editExpenseDescriptor; + + /** + * @param index of the expense in the filtered expense list to edit + * @param editExpenseDescriptor details to edit the expense with + */ + public EditExpenseCommand(Index index, EditExpenseDescriptor editExpenseDescriptor) { + requireNonNull(index); + requireNonNull(editExpenseDescriptor); + + this.index = index; + this.editExpenseDescriptor = new EditExpenseDescriptor(editExpenseDescriptor); + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredExpenseList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXPENSE_DISPLAYED_INDEX); + } + + Expense toEdit = lastShownList.get(index.getZeroBased()); + if (toEdit instanceof Allowance) { + throw new CommandException(MESSAGE_ITEM_NOT_EXPENSE); + } + Expense editedExpense = createEditedExpense(toEdit, editExpenseDescriptor); + + model.setExpense(toEdit, editedExpense); + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + model.commitEPiggy(); + return new CommandResult(String.format(MESSAGE_EDIT_EXPENSE_SUCCESS, editedExpense)); + } + + /** + * Creates and returns a {@code Expense} with the details of {@code expenseToEdit} + * edited with {@code editExpenseDescriptor}. + */ + static Expense createEditedExpense(Expense expenseToEdit, EditExpenseDescriptor editExpenseDescriptor) { + assert expenseToEdit != null; + + Name updatedName = editExpenseDescriptor.getName().orElse(expenseToEdit.getItem().getName()); + Cost updatedCost = editExpenseDescriptor.getCost().orElse(expenseToEdit.getItem().getCost()); + Date updatedDate = editExpenseDescriptor.getDate().orElse(expenseToEdit.getDate()); + Set updatedTags = editExpenseDescriptor.getTags().orElse(expenseToEdit.getItem().getTags()); + return new Expense(new Item(updatedName, updatedCost, updatedTags), updatedDate); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditExpenseCommand)) { + return false; + } + + // state check + EditExpenseCommand e = (EditExpenseCommand) other; + return index.equals(e.index) + && editExpenseDescriptor.equals(e.editExpenseDescriptor); + } + + /** + * 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 EditExpenseDescriptor { + + private seedu.address.model.epiggy.item.Name name; + private Cost cost; + private Date date; + private Set tags; + + public EditExpenseDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditExpenseDescriptor(EditExpenseDescriptor toCopy) { + setName(toCopy.name); + setCost(toCopy.cost); + setDate(toCopy.date); + setTags(toCopy.tags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, cost, date, tags); + } + + public void setName(seedu.address.model.epiggy.item.Name name) { + this.name = name; + } + + public void setCost(Cost cost) { + this.cost = cost; + } + + public void setDate(Date date) { + this.date = date; + } + + /** + * 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 Optional getName() { + return Optional.ofNullable(name); + } + + public Optional getCost() { + return Optional.ofNullable(cost); + } + + public Optional getDate() { + return Optional.ofNullable(date); + } + + /** + * 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() { + if (tags != null) { + tags.add(new Tag("Expense")); + return Optional.of(Collections.unmodifiableSet(tags)); + } else { + return Optional.empty(); + } + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditExpenseDescriptor)) { + return false; + } + + // state check + EditExpenseDescriptor e = (EditExpenseDescriptor) other; + + return getName().equals(e.getName()) + && getCost().equals(e.getCost()) + && getDate().equals(e.getDate()) + && getTags().equals(e.getTags()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/FindCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/FindCommand.java new file mode 100644 index 000000000000..e69bbae41c76 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/FindCommand.java @@ -0,0 +1,52 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_EXPENSES_LISTED_OVERVIEW; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; +import seedu.address.model.epiggy.ExpenseContainsKeywordsPredicate; + + +//@@author rahulb99 +/** + * Finds and lists all expenses in ePiggy whose expense contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FindCommand extends Command { + + public static final String COMMAND_WORD = "find"; + public static final String COMMAND_ALIAS = "fd"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Finds the expense as specified by the user. " + + " The keywords do not need to be in order.\n" + + " Parameters: [n/NAME] [$/COST:COST] [t/TAG] [d/DATE:DATE]...\n" + + " Example: " + COMMAND_WORD + " n/Mala Hotpot t/lunch t/food $/7.00:15.00 d/14/03/2019:17/03/2019\n"; + public static final String MESSAGE_SUCCESS = MESSAGE_EXPENSES_LISTED_OVERVIEW; + + private final ExpenseContainsKeywordsPredicate predicate; + + public FindCommand(ExpenseContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + model.updateFilteredExpensesList(predicate); + model.commitEPiggy(); + + return new CommandResult( + String.format(MESSAGE_SUCCESS, model.getFilteredExpenseList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindCommand // instanceof handles nulls + && predicate.equals(((FindCommand) other).predicate)); // state check + } + +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/ReportCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/ReportCommand.java new file mode 100644 index 000000000000..ad9f0a803a94 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/ReportCommand.java @@ -0,0 +1,62 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; + +import java.time.LocalDate; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; +import seedu.address.ui.ReportWindow; + +/** + * Shows summary to the user. + */ +//@@author yunjun199321 +public class ReportCommand extends Command { + public static final String COMMAND_WORD = "report"; + public static final String COMMAND_ALIAS = "rp"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Shows the report.\n" + + "Parameters: " + + PREFIX_DATE + "SPECIFY DATE, MONTH OR YEAR \n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_DATE + "21/03/2019\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_DATE + "03/2019\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_DATE + "2019\n"; + + public static final String MESSAGE_SUCCESS = "Report shown!"; + + //private final Logger logger = LogsCenter.getLogger(getClass()); + + private String type = "ALL"; + private LocalDate date; + + /** + * Constructor with chart type. + * @param date + */ + public ReportCommand(LocalDate date, String type) { + this.date = date; + this.type = type; + } + public String getType() { + return type; + } + + public LocalDate getDate() { + return date; + } + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + ReportWindow summaryWindow = new ReportWindow(); + summaryWindow.displayReportController(model, date, type); + return new CommandResult(MESSAGE_SUCCESS, false, false, true); + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/ReverseListCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/ReverseListCommand.java new file mode 100644 index 000000000000..c4f05e70c6b9 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/ReverseListCommand.java @@ -0,0 +1,30 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; + +//@@author rahulb99 +/** + * Lists all Expenses in reverse order to the user. + */ +public class ReverseListCommand extends Command { + + public static final String COMMAND_WORD = "reverseList"; + public static final String COMMAND_ALIAS = "rl"; + + public static final String MESSAGE_SUCCESS = "Listed all expenses in reverse."; + + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + model.reverseFilteredExpensesList(); + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/SetGoalCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/SetGoalCommand.java new file mode 100644 index 000000000000..8100a6badd56 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/SetGoalCommand.java @@ -0,0 +1,53 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Goal; + +//@@author kev-inc + +/** + * Set goal amount and name. + */ +public class SetGoalCommand extends Command { + public static final String COMMAND_WORD = "setGoal"; + public static final String COMMAND_ALIAS = "sg"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Sets a goal that you would like to save up to. " + + "Parameters: " + + PREFIX_NAME + "GOAL NAME " + + PREFIX_COST + "GOAL AMOUNT\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "Nike shoe " + + PREFIX_COST + "79"; + public static final String MESSAGE_SUCCESS = "Goal set.\nGoal's details:\n%1$s"; + + private final Goal toSet; + + public SetGoalCommand(Goal toSet) { + requireNonNull(toSet); + this.toSet = toSet; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + model.setGoal(toSet); + model.commitEPiggy(); + return new CommandResult(String.format(MESSAGE_SUCCESS, toSet)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SetGoalCommand // instanceof handles nulls + && toSet.equals(((SetGoalCommand) other).toSet)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/SortCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/SortCommand.java new file mode 100644 index 000000000000..58940800063a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/SortCommand.java @@ -0,0 +1,73 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; + +import java.util.Comparator; +import java.util.Objects; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Expense; + + +//@@author rahulb99 +/** + * Finds and lists all expenses in EPiggy whose expense contains any of the argument keywordss. + * keywords matching is case insensitive. + */ +public class SortCommand extends Command { + + public static final String COMMAND_WORD = "sort"; + public static final String COMMAND_ALIAS = "st"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + " : Sorts the expenses as specified by the user, according to name, cost, or date. \n" + + " Parameters: [n/d/$]/...\n" + + " Example: " + COMMAND_WORD + " n/"; + public static final String MESSAGE_SUCCESS = "Sorted %1$d expenses."; + + private final Comparator expenseComparator; + + public SortCommand(Comparator expenseComparator) { + assert expenseComparator != null : "keywords should not be null."; + this.expenseComparator = expenseComparator; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + model.sortExpenses(expenseComparator); + model.updateFilteredExpensesList(Model.PREDICATE_SHOW_ALL_EXPENSES); + model.commitEPiggy(); + + return new CommandResult( + String.format(MESSAGE_SUCCESS, model.getFilteredExpenseList().size())); + } + /* + public Comparator getExpenseComparator() { + return expenseComparator; + } + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SortCommand that = (SortCommand) o; + return Objects.equals(expenseComparator, that.expenseComparator); + } + + @Override + public int hashCode() { + return Objects.hash(expenseComparator); + } + + @Override + public String toString() { + return expenseComparator.toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/ViewGoalCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/ViewGoalCommand.java new file mode 100644 index 000000000000..7db175e152e2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/ViewGoalCommand.java @@ -0,0 +1,52 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; + +import javafx.beans.property.SimpleObjectProperty; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Goal; +import seedu.address.model.epiggy.item.Cost; + +//@@author kev-inc + +/** + * Views the current goal set. + */ +public class ViewGoalCommand extends Command { + + public static final String COMMAND_WORD = "viewGoal"; + public static final String COMMAND_ALIAS = "vg"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": View your goal set. "; + + public static final String MESSAGE_SUCCESS = "Your current goal is: %1$s\n"; + + public static final String MESSAGE_SAVINGS_LESS_THAN_GOAL = "$%2$s more to go!"; + public static final String MESSAGE_SAVINGS_MORE_THAN_GOAL = "You have reached your savings goal! Congratulations!"; + + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + SimpleObjectProperty savings = model.getSavings(); + SimpleObjectProperty goal = model.getGoal(); + + double goalAmount = goal.getValue().getAmount().getAmount(); + double savingsAmount = savings.getValue().getAmount(); + + double diff = goalAmount - savingsAmount; + + if (diff < 0) { + return new CommandResult(String.format(MESSAGE_SUCCESS + + MESSAGE_SAVINGS_MORE_THAN_GOAL, goal.getValue())); + } else { + return new CommandResult(String.format(MESSAGE_SUCCESS + + MESSAGE_SAVINGS_LESS_THAN_GOAL, goal.getValue(), diff)); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/epiggy/ViewSavingsCommand.java b/src/main/java/seedu/address/logic/commands/epiggy/ViewSavingsCommand.java new file mode 100644 index 000000000000..c14c2d77b210 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/epiggy/ViewSavingsCommand.java @@ -0,0 +1,35 @@ +package seedu.address.logic.commands.epiggy; + +import static java.util.Objects.requireNonNull; + +import javafx.beans.property.SimpleObjectProperty; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.epiggy.item.Cost; + +//@@author kev-inc + +/** + * Allows the user to view the savings recorded in ePiggy. + */ +public class ViewSavingsCommand extends Command { + + public static final String COMMAND_WORD = "viewSavings"; + public static final String COMMAND_ALIAS = "vs"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": View your savings. "; + + public static final String MESSAGE_SUCCESS = "Your savings are %1$s"; + + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + + requireNonNull(model); + SimpleObjectProperty savings = model.getSavings(); + return new CommandResult(String.format(MESSAGE_SUCCESS, savings.getValue())); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java deleted file mode 100644 index b7d57f5db86a..000000000000 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ /dev/null @@ -1,92 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.HistoryCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.commands.RedoCommand; -import seedu.address.logic.commands.SelectCommand; -import seedu.address.logic.commands.UndoCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses user input. - */ -public class AddressBookParser { - - /** - * Used for initial separation of command word and args. - */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - - /** - * Parses user input into command for execution. - * - * @param userInput full user input string - * @return the command based on the user input - * @throws ParseException if the user input does not conform the expected format - */ - public Command parseCommand(String userInput) throws ParseException { - final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); - if (!matcher.matches()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); - switch (commandWord) { - - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); - - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); - - case SelectCommand.COMMAND_WORD: - return new SelectCommandParser().parse(arguments); - - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); - - case ClearCommand.COMMAND_WORD: - return new ClearCommand(); - - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - - case HistoryCommand.COMMAND_WORD: - return new HistoryCommand(); - - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); - - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - - case UndoCommand.COMMAND_WORD: - return new UndoCommand(); - - case RedoCommand.COMMAND_WORD: - return new RedoCommand(); - - default: - throw new ParseException(MESSAGE_UNKNOWN_COMMAND); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java index 954c8e18f8ea..a32d020c6227 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; /** @@ -57,4 +58,16 @@ public List getAllValues(Prefix prefix) { public String getPreamble() { return getValue(new Prefix("")).orElse(""); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ArgumentMultimap that = (ArgumentMultimap) o; + return Objects.equals(argMultimap, that.argMultimap); + } } diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf1190..8a2d081dc46d 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -12,4 +12,8 @@ public class CliSyntax { public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_COST = new Prefix("$/"); + public static final Prefix PREFIX_CATEGORY = new Prefix("c/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_PERIOD = new Prefix("p/"); } diff --git a/src/main/java/seedu/address/logic/parser/EPiggyParser.java b/src/main/java/seedu/address/logic/parser/EPiggyParser.java new file mode 100644 index 000000000000..0b7fa44cb61c --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EPiggyParser.java @@ -0,0 +1,166 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.HistoryCommand; +import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.UndoCommand; +import seedu.address.logic.commands.epiggy.AddAllowanceCommand; +import seedu.address.logic.commands.epiggy.AddBudgetCommand; +import seedu.address.logic.commands.epiggy.AddExpenseCommand; +import seedu.address.logic.commands.epiggy.DeleteAllowanceCommand; +import seedu.address.logic.commands.epiggy.DeleteBudgetCommand; +import seedu.address.logic.commands.epiggy.DeleteExpenseCommand; +import seedu.address.logic.commands.epiggy.EditAllowanceCommand; +import seedu.address.logic.commands.epiggy.EditBudgetCommand; +import seedu.address.logic.commands.epiggy.EditExpenseCommand; +import seedu.address.logic.commands.epiggy.FindCommand; +import seedu.address.logic.commands.epiggy.ReportCommand; +import seedu.address.logic.commands.epiggy.ReverseListCommand; +import seedu.address.logic.commands.epiggy.SetGoalCommand; +import seedu.address.logic.commands.epiggy.SortCommand; +import seedu.address.logic.parser.epiggy.AddAllowanceCommandParser; +import seedu.address.logic.parser.epiggy.AddBudgetCommandParser; +import seedu.address.logic.parser.epiggy.AddExpenseCommandParser; +import seedu.address.logic.parser.epiggy.DeleteAllowanceCommandParser; +import seedu.address.logic.parser.epiggy.DeleteBudgetCommandParser; +import seedu.address.logic.parser.epiggy.DeleteExpenseCommandParser; +import seedu.address.logic.parser.epiggy.EditAllowanceCommandParser; +import seedu.address.logic.parser.epiggy.EditBudgetCommandParser; +import seedu.address.logic.parser.epiggy.EditExpenseCommandParser; +import seedu.address.logic.parser.epiggy.FindCommandParser; +import seedu.address.logic.parser.epiggy.ReportCommandParser; +import seedu.address.logic.parser.epiggy.SetGoalCommandParser; +import seedu.address.logic.parser.epiggy.SortCommandParser; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses user input. + */ +public class EPiggyParser { + + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + switch (commandWord) { + + case AddExpenseCommand.COMMAND_WORD: + case AddExpenseCommand.COMMAND_ALIAS: + return new AddExpenseCommandParser().parse(arguments); + + case EditExpenseCommand.COMMAND_WORD: + case EditExpenseCommand.COMMAND_ALIAS: + return new EditExpenseCommandParser().parse(arguments); + + case DeleteExpenseCommand.COMMAND_WORD: + case DeleteExpenseCommand.COMMAND_ALIAS: + return new DeleteExpenseCommandParser().parse(arguments); + + case AddAllowanceCommand.COMMAND_WORD: + case AddAllowanceCommand.COMMAND_ALIAS: + return new AddAllowanceCommandParser().parse(arguments); + + case EditAllowanceCommand.COMMAND_WORD: + case EditAllowanceCommand.COMMAND_ALIAS: + return new EditAllowanceCommandParser().parse(arguments); + + case DeleteAllowanceCommand.COMMAND_WORD: + case DeleteAllowanceCommand.COMMAND_ALIAS: + return new DeleteAllowanceCommandParser().parse(arguments); + + case FindCommand.COMMAND_WORD: + case FindCommand.COMMAND_ALIAS: + return new FindCommandParser().parse(arguments); + + case SortCommand.COMMAND_WORD: + case SortCommand.COMMAND_ALIAS: + return new SortCommandParser().parse(arguments); + + case ReverseListCommand.COMMAND_WORD: + case ReverseListCommand.COMMAND_ALIAS: + return new ReverseListCommand(); + + case ClearCommand.COMMAND_WORD: + case ClearCommand.COMMAND_ALIAS: + return new ClearCommand(); + + case ListCommand.COMMAND_WORD: + case ListCommand.COMMAND_ALIAS: + return new ListCommand(); + + case HistoryCommand.COMMAND_WORD: + case HistoryCommand.COMMAND_ALIAS: + return new HistoryCommand(); + + case ExitCommand.COMMAND_WORD: + case ExitCommand.COMMAND_ALIAS: + return new ExitCommand(); + + case HelpCommand.COMMAND_WORD: + case HelpCommand.COMMAND_ALIAS: + return new HelpCommand(); + + case AddBudgetCommand.COMMAND_WORD: + case AddBudgetCommand.COMMAND_ALIAS: + return new AddBudgetCommandParser().parse(arguments); + + case EditBudgetCommand.COMMAND_WORD: + case EditBudgetCommand.COMMAND_ALIAS: + return new EditBudgetCommandParser().parse(arguments); + + case DeleteBudgetCommand.COMMAND_WORD: + case DeleteBudgetCommand.COMMAND_ALIAS: + return new DeleteBudgetCommandParser().parse(arguments); + + case SetGoalCommand.COMMAND_WORD: + case SetGoalCommand.COMMAND_ALIAS: + return new SetGoalCommandParser().parse(arguments); + + case UndoCommand.COMMAND_WORD: + case UndoCommand.COMMAND_ALIAS: + return new UndoCommand(); + + case RedoCommand.COMMAND_WORD: + case RedoCommand.COMMAND_ALIAS: + return new RedoCommand(); + + case ReportCommand.COMMAND_WORD: + case ReportCommand.COMMAND_ALIAS: + if (arguments.equals("")) { + return new ReportCommandParser().parse(""); + } else { + return new ReportCommandParser().parse(arguments); + } + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55b..387ada6de318 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -1,17 +1,30 @@ package seedu.address.logic.parser; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_CANNOT_CREATE_ALLOWANCE_TAG; +import static seedu.address.commons.core.Messages.MESSAGE_CANNOT_CREATE_EXPENSE_TAG; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.text.SimpleDateFormat; import java.util.Collection; +import java.util.Date; import java.util.HashSet; +import java.util.List; 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.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Name; +import seedu.address.model.epiggy.item.Period; 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; @@ -22,6 +35,7 @@ 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. @@ -35,21 +49,6 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } - /** - * Parses a {@code String name} into a {@code Name}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code name} is invalid. - */ - public static Name parseName(String name) throws ParseException { - requireNonNull(name); - String trimmedName = name.trim(); - if (!Name.isValidName(trimmedName)) { - throw new ParseException(Name.MESSAGE_CONSTRAINTS); - } - return new Name(trimmedName); - } - /** * Parses a {@code String phone} into a {@code Phone}. * Leading and trailing whitespaces will be trimmed. @@ -95,6 +94,75 @@ public static Email parseEmail(String email) throws ParseException { return new Email(trimmedEmail); } + /** + * Parses a {@code String name} into a {@code Name}. + * Leading and trailing whitespaces will be trimmed. + */ + public static seedu.address.model.person.Name parseName(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!seedu.address.model.person.Name.isValidName(trimmedName)) { + throw new ParseException(seedu.address.model.person.Name.MESSAGE_CONSTRAINTS); + } + return new seedu.address.model.person.Name(trimmedName); + } + + /** + * Parses a {@code String name} into a {@code Name}. + * Leading and trailing whitespaces will be trimmed. + */ + public static Name parseItemName(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!Name.isValidName(trimmedName)) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS); + } + return new Name(trimmedName); + } + + /** + * Parses a {@code String cost} into a {@code Cost}. + * Leading and trailing whitespaces will be trimmed. + */ + public static Cost parseCost(String cost) throws ParseException { + requireNonNull(cost); + String trimmedCost = cost.trim(); + if (!Cost.isValidCost(trimmedCost)) { + throw new ParseException(Cost.MESSAGE_CONSTRAINTS); + } + return new Cost(Double.parseDouble(trimmedCost)); + } + + /** + * Parses a {@code String Date} into a {@code Date}. + * Leading and trailing whitespaces will be trimmed. + */ + public static Date parseDate(String date) throws ParseException { + requireNonNull(date); + Date parsedDate; + SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy"); + dateFormat.setLenient(false); + try { + parsedDate = dateFormat.parse(date.trim()); + } catch (java.text.ParseException parseException) { + throw new ParseException(MESSAGE_INVALID_DATE); + } + return parsedDate; + } + + /** + * Parses a {@code String period} into a {@code Period}. + * Leading and trailing whitespaces will be trimmed. + */ + public static Period parsePeriod(String period) throws ParseException { + requireNonNull(period); + String trimmedPeriod = period.trim(); + if (!Period.isValidPeriod(trimmedPeriod)) { + throw new ParseException(Period.MESSAGE_CONSTRAINTS); + } + return new Period(Integer.parseInt(trimmedPeriod)); + } + /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. @@ -107,6 +175,11 @@ public static Tag parseTag(String tag) throws ParseException { if (!Tag.isValidTagName(trimmedTag)) { throw new ParseException(Tag.MESSAGE_CONSTRAINTS); } + if (tag.toLowerCase().equals("allowance")) { + throw new ParseException(MESSAGE_CANNOT_CREATE_ALLOWANCE_TAG); + } else if (tag.toLowerCase().equals("expense")) { + throw new ParseException(MESSAGE_CANNOT_CREATE_EXPENSE_TAG); + } return new Tag(trimmedTag); } @@ -121,4 +194,146 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Keywords validation. + * @param keywordsMap user input to keyword mapping. + * @throws ParseException if any of the keywords are invalid. + */ + public static void validateKeywords(ArgumentMultimap keywordsMap) throws ParseException { + List nameKeywords = keywordsMap.getAllValues(PREFIX_NAME); + List tagKeywords = keywordsMap.getAllValues(PREFIX_TAG); + List dateKeywords = keywordsMap.getAllValues(PREFIX_DATE); + List costKeywords = keywordsMap.getAllValues(PREFIX_COST); + + validateNameKeywords(nameKeywords); + validateTagKeywords(tagKeywords); + validateDateKeywords(dateKeywords); + validateCostKeywords(costKeywords); + } + + /** + * Keywords validation for sort. + * @param keywordsMap user input to keyword mapping. + * @throws ParseException if any of the keywords are invalid. + */ + public static void validateKeywordsForSort(ArgumentMultimap keywordsMap) throws ParseException { + List nameKeywords = keywordsMap.getAllValues(PREFIX_NAME); + List tagKeywords = keywordsMap.getAllValues(PREFIX_TAG); + List dateKeywords = keywordsMap.getAllValues(PREFIX_DATE); + List costKeywords = keywordsMap.getAllValues(PREFIX_COST); + + if (!nameKeywords.isEmpty() && !tagKeywords.isEmpty() && !dateKeywords.isEmpty() && !costKeywords.isEmpty()) { + throw new ParseException("Invalid userInput"); + } + } + + /** + * Name keyword validation. + * @throws ParseException if name keyword is invalid (not alphanumeric). + */ + private static void validateNameKeywords(List nameKeywords) throws ParseException { + for (String name: nameKeywords) { + if (!Name.isValidName(name)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, Name.MESSAGE_CONSTRAINTS)); + } + } + } + + /** + * Tag keyword validation. + * @throws ParseException if tag keyword is invalid (not alphanumeric). + */ + private static void validateTagKeywords(List tagKeywords) throws ParseException { + for (String tag: tagKeywords) { + if (!Tag.isValidTagName(tag)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, Tag.MESSAGE_CONSTRAINTS)); + } + } + } + + /** + * Name keyword validation. + * @throws ParseException if name keyword is invalid (not alphanumeric). + */ + private static void validateDateKeywords(List dateKeywords) throws ParseException { + for (String d: dateKeywords) { + String d1 = d.split(":")[0]; + String d2 = d.split(":").length == 2 ? d.split(":")[1] : ""; + + //If the user enters one date keyword and it is invalid + if (d2.equals("") && !isValidDate(d1)) { + throw new ParseException("Invalid Date"); + } + + //If the user enters a range of date keywords and any of the two dates is invalid + if (!d2.equals("") && (!isValidDate(d1) || !isValidDate(d2))) { + throw new ParseException("Invalid Date"); + } + + //If the ending date is earlier than the starting date + if (!d2.equals("")) { + Date date1 = new Date(d1); + Date date2 = new Date(d2); + if (date1.after(date2)) { + throw new ParseException("Invalid Date"); + } + } + + //If the user enters more than one colon + if (d.split(":").length > 2) { + throw new ParseException("Invalid Date"); + } + } + } + + /** + * Date validation. + * @param date input date + * @return true if date is valid, false otherwise. + * @throws ParseException if date is valid. + */ + private static boolean isValidDate(String date) throws ParseException { + SimpleDateFormat sdfrmt = new SimpleDateFormat("dd/MM/yyyy"); + sdfrmt.setLenient(false); + try { + Date d = sdfrmt.parse(date); + } catch (java.text.ParseException e) { + return false; + } + return true; + } + + /** + * Name keyword validation. + * @throws ParseException if name keyword is invalid (not alphanumeric). + */ + private static void validateCostKeywords(List costKeywords) throws ParseException { + for (String cost : costKeywords) { + String c1 = cost.split(":")[0]; + String c2 = cost.split(":").length == 2 ? cost.split(":")[1] : ""; + + //If the user enters one cost keyword and it is invalid + if (c2.equals("") && !Cost.isValidCost(c1)) { + throw new ParseException(Cost.MESSAGE_CONSTRAINTS); + } + + //If the user enters a range of cost keywords and any of the two cost is invalid + if (!c2.equals("") && (!Cost.isValidCost(c1) || !Cost.isValidCost(c2))) { + throw new ParseException(Cost.MESSAGE_CONSTRAINTS); + } + + //If cost2 is lesser than cost1 + if (!c2.equals("")) { + if (Float.parseFloat(c1) > Float.parseFloat(c2)) { + throw new ParseException(Cost.MESSAGE_CONSTRAINTS); + } + } + + //If the user enters more than one colon + if (cost.split(":").length > 2) { + throw new ParseException(Cost.MESSAGE_CONSTRAINTS); + } + } + } } diff --git a/src/main/java/seedu/address/logic/parser/epiggy/AddAllowanceCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/AddAllowanceCommandParser.java new file mode 100644 index 000000000000..0d4ebda548a2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/AddAllowanceCommandParser.java @@ -0,0 +1,68 @@ +package seedu.address.logic.parser.epiggy; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Date; +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.epiggy.AddAllowanceCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Item; +import seedu.address.model.epiggy.item.Name; +import seedu.address.model.tag.Tag; + +//@@author kev-inc + +/** + * Parses input arguments and creates a new AddAllowanceCommand object. + */ +public class AddAllowanceCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddAllowanceCommand + * and returns an AddAllowanceCommand object for execution. + */ + @Override + public AddAllowanceCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_COST, PREFIX_DATE, PREFIX_TAG); + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_COST) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddAllowanceCommand.MESSAGE_USAGE)); + } + + Name name = ParserUtil.parseItemName(argMultimap.getValue(PREFIX_NAME).get()); + Cost cost = ParserUtil.parseCost(argMultimap.getValue(PREFIX_COST).get()); + Date date = new Date(); + if (arePrefixesPresent(argMultimap, PREFIX_DATE)) { + date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + } + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + tagList.add(new Tag("Allowance")); + + Item item = new Item(name, cost, tagList); + Allowance allowance = new Allowance(item, date); + + return new AddAllowanceCommand(allowance); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/epiggy/AddBudgetCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/AddBudgetCommandParser.java new file mode 100644 index 000000000000..eb0ca60014df --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/AddBudgetCommandParser.java @@ -0,0 +1,59 @@ +package seedu.address.logic.parser.epiggy; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERIOD; + +import java.util.Date; +import java.util.stream.Stream; + +import seedu.address.logic.commands.epiggy.AddBudgetCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.epiggy.Budget; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Period; + +//@@author tehwenyi + +/** + * Parses input arguments and creates a new AddBudgetCommand object + */ +public class AddBudgetCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddBudgetCommand + * and returns an AddBudgetCommand object for execution. + */ + public AddBudgetCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_COST, PREFIX_PERIOD, PREFIX_DATE); + + if (!arePrefixesPresent(argMultimap, PREFIX_COST, PREFIX_PERIOD, PREFIX_DATE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddBudgetCommand.MESSAGE_USAGE)); + } + + Cost budgetAmount = ParserUtil.parseCost(argMultimap.getValue(PREFIX_COST).get()); + Period period = ParserUtil.parsePeriod(argMultimap.getValue(PREFIX_PERIOD).get()); + Date date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + + Budget budget = new Budget(budgetAmount, period, date); + + return new AddBudgetCommand(budget); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/epiggy/AddExpenseCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/AddExpenseCommandParser.java new file mode 100644 index 000000000000..d99d93225dcd --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/AddExpenseCommandParser.java @@ -0,0 +1,66 @@ +package seedu.address.logic.parser.epiggy; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Date; +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.epiggy.AddExpenseCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Item; +import seedu.address.model.epiggy.item.Name; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new AddExpenseCommand object + */ +public class AddExpenseCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddExpenseCommand + * and returns an AddCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public AddExpenseCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_COST, PREFIX_DATE, PREFIX_TAG); + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_COST) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddExpenseCommand.MESSAGE_USAGE)); + } + + Name name = ParserUtil.parseItemName(argMultimap.getValue(PREFIX_NAME).get()); + Cost cost = ParserUtil.parseCost(argMultimap.getValue(PREFIX_COST).get()); + Date date = new Date(); + if (arePrefixesPresent(argMultimap, PREFIX_DATE)) { + date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + } + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + tagList.add(new Tag("Expense")); + + Item item = new Item(name, cost, tagList); + Expense expense = new Expense(item, date); + return new AddExpenseCommand(expense); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/epiggy/DeleteAllowanceCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/DeleteAllowanceCommandParser.java new file mode 100644 index 000000000000..b109feae0981 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/DeleteAllowanceCommandParser.java @@ -0,0 +1,33 @@ +package seedu.address.logic.parser.epiggy; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.epiggy.DeleteAllowanceCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author kev-inc + +/** + * Parses input arguments and creates a new DeleteAllowanceCommand object + */ +public class DeleteAllowanceCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteAllowanceCommand + * and returns an DeleteAllowanceCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public DeleteAllowanceCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteAllowanceCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteAllowanceCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/epiggy/DeleteBudgetCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/DeleteBudgetCommandParser.java new file mode 100644 index 000000000000..265e2b739905 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/DeleteBudgetCommandParser.java @@ -0,0 +1,33 @@ +package seedu.address.logic.parser.epiggy; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.epiggy.DeleteBudgetCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author tehwenyi + +/** + * Parses input arguments and creates a new DeleteBudgetCommand object + */ +public class DeleteBudgetCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteBudgetCommand + * and returns an DeleteBudgetCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteBudgetCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteBudgetCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteBudgetCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/epiggy/DeleteExpenseCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/DeleteExpenseCommandParser.java new file mode 100644 index 000000000000..d51832767a30 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/DeleteExpenseCommandParser.java @@ -0,0 +1,32 @@ +package seedu.address.logic.parser.epiggy; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.epiggy.DeleteExpenseCommand; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteExpenseCommand object + */ +public class DeleteExpenseCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteBudgetCommand + * and returns an DeleteBudgetCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteExpenseCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteExpenseCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteExpenseCommand.MESSAGE_USAGE), pe); + } + } + +} + diff --git a/src/main/java/seedu/address/logic/parser/epiggy/EditAllowanceCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/EditAllowanceCommandParser.java new file mode 100644 index 000000000000..bbeeedc15010 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/EditAllowanceCommandParser.java @@ -0,0 +1,85 @@ +package seedu.address.logic.parser.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.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.epiggy.EditAllowanceCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +//@@author kev-inc + +/** + * Parses input arguments and creates a new EditAllowanceCommand object + */ +public class EditAllowanceCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditAllowanceCommand + * and returns an EditAllowanceCommand object for execution. + */ + @Override + public EditAllowanceCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_COST, PREFIX_DATE, PREFIX_TAG); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException( + String.format(EditAllowanceCommand.MESSAGE_NOT_EDITED, + EditAllowanceCommand.MESSAGE_USAGE), + pe); + } + + EditAllowanceCommand.EditAllowanceDescriptor editAllowanceDescriptor = new EditAllowanceCommand + .EditAllowanceDescriptor(); + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + editAllowanceDescriptor.setName(ParserUtil.parseItemName(argMultimap.getValue(PREFIX_NAME).get())); + } + if (argMultimap.getValue(PREFIX_COST).isPresent()) { + editAllowanceDescriptor.setCost(ParserUtil.parseCost(argMultimap.getValue(PREFIX_COST).get())); + } + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + editAllowanceDescriptor.setDate(ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get())); + } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editAllowanceDescriptor::setTags); + + if (!editAllowanceDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditAllowanceCommand.MESSAGE_NOT_EDITED); + } + + return new EditAllowanceCommand(index, editAllowanceDescriptor); + } + + /** + * 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; + + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/epiggy/EditBudgetCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/EditBudgetCommandParser.java new file mode 100644 index 000000000000..4bc8dd192831 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/EditBudgetCommandParser.java @@ -0,0 +1,51 @@ +package seedu.address.logic.parser.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PERIOD; + +import seedu.address.logic.commands.epiggy.EditBudgetCommand; +import seedu.address.logic.commands.epiggy.EditBudgetCommand.EditBudgetDetails; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author tehwenyi + +/** + * Parses input arguments and creates a new EditBudgetCommand object + */ +public class EditBudgetCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditBudgetCommand + * and returns an EditBudgetCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditBudgetCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_COST, PREFIX_PERIOD, PREFIX_DATE); + + EditBudgetDetails editBudgetDetails = new EditBudgetDetails(); + if (argMultimap.getValue(PREFIX_COST).isPresent()) { + editBudgetDetails.setAmount(ParserUtil.parseCost(argMultimap.getValue(PREFIX_COST).get())); + } + if (argMultimap.getValue(PREFIX_PERIOD).isPresent()) { + editBudgetDetails.setPeriod(ParserUtil.parsePeriod(argMultimap.getValue(PREFIX_PERIOD).get())); + } + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + editBudgetDetails.setStartDate(ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get())); + } + + if (!editBudgetDetails.isAnyFieldEdited()) { + throw new ParseException(EditBudgetCommand.MESSAGE_NOT_EDITED); + } + + return new EditBudgetCommand(editBudgetDetails); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/epiggy/EditExpenseCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/EditExpenseCommandParser.java new file mode 100644 index 000000000000..95c708ed5baa --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/EditExpenseCommandParser.java @@ -0,0 +1,83 @@ +package seedu.address.logic.parser.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.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.epiggy.EditExpenseCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new EditExpenseCommand object + */ +public class EditExpenseCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditExpenseCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_COST, PREFIX_DATE, PREFIX_TAG); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditExpenseCommand.MESSAGE_USAGE), + pe); + } + + EditExpenseCommand.EditExpenseDescriptor editExpenseDescriptor = new EditExpenseCommand.EditExpenseDescriptor(); + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + editExpenseDescriptor.setName(ParserUtil.parseItemName(argMultimap.getValue(PREFIX_NAME).get())); + } + if (argMultimap.getValue(PREFIX_COST).isPresent()) { + editExpenseDescriptor.setCost(ParserUtil.parseCost(argMultimap.getValue(PREFIX_COST).get())); + } + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + editExpenseDescriptor.setDate(ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get())); + } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editExpenseDescriptor::setTags); + + if (!editExpenseDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditExpenseCommand.MESSAGE_NOT_EDITED); + } + + return new EditExpenseCommand(index, editExpenseDescriptor); + } + + /** + * 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; + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/epiggy/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/FindCommandParser.java new file mode 100644 index 000000000000..ed073462535e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/FindCommandParser.java @@ -0,0 +1,58 @@ +package seedu.address.logic.parser.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.ParserUtil.validateKeywords; + +import seedu.address.logic.commands.epiggy.FindCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.epiggy.ExpenseContainsKeywordsPredicate; + +//@@author rahulb99 +/** + * Parses input arguments and creates a new FindCommand object. + */ +public class FindCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns an FindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindCommand parse(String args) throws ParseException { + requireNonNull(args); + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + + //Check whether the user follow the pattern + if (!trimmedArgs.contains("/")) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + + String[] splitTrimmedArgs = trimmedArgs.split("/"); + if (splitTrimmedArgs[0].equals("")) { + //Ensure args contains at least one prefix + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + + ArgumentMultimap keywordsMap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_COST, PREFIX_TAG, PREFIX_DATE); + validateKeywords(keywordsMap); + + ExpenseContainsKeywordsPredicate predicate = new ExpenseContainsKeywordsPredicate(keywordsMap); + return new FindCommand(predicate); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/epiggy/ReportCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/ReportCommandParser.java new file mode 100644 index 000000000000..679b7377e6cd --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/ReportCommandParser.java @@ -0,0 +1,87 @@ +package seedu.address.logic.parser.epiggy; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; + +import java.time.LocalDate; +import java.util.stream.Stream; + +import seedu.address.logic.commands.epiggy.ReportCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and display report. + */ +//@@author yunjun199321 +public class ReportCommandParser implements Parser { + + /** + * 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()); + } + + /** + * Parses the given {@code String} of arguments in the context of the ReportCommand + * and returns an ReportCommand object for execution. + */ + @Override + public ReportCommand parse(String args) throws ParseException { + LocalDate date; + int day = 1; + int month = 1; + int year = 1970; + String[] type = {"YEAR", "MONTH", "DAY", "ALL"}; + + if (args.equals("")) { + // no parameter found + return new ReportCommand(null, type[3]); + } + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_DATE); + + if (!arePrefixesPresent(argMultimap, PREFIX_DATE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ReportCommand.MESSAGE_USAGE)); + } + String dateString = argMultimap.getValue(PREFIX_DATE).get(); + // splits the dateString into year, month and day. + String[] dateArr = dateString.split("/"); + try { + if (dateArr.length == 3) { + // date string contains year, month and day + day = Integer.valueOf(dateArr[0]); + month = Integer.valueOf(dateArr[1]); + year = Integer.valueOf(dateArr[2]); + date = LocalDate.of(year, month, day); + return new ReportCommand(date, type[2]); + } else if (dateArr.length == 2) { + // date string only contains month and year + month = Integer.valueOf(dateArr[0]); + year = Integer.valueOf(dateArr[1]); + date = LocalDate.of(year, month, day); + return new ReportCommand(date, type[1]); + } else if (dateArr.length == 1) { + year = Integer.valueOf(dateArr[0]); + date = LocalDate.of(year, month, day); + return new ReportCommand(date, type[0]); + } else { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ReportCommand.MESSAGE_USAGE)); + } + } catch (Exception e) { + throw new ParseException(String.format(MESSAGE_INVALID_DATE, + ReportCommand.MESSAGE_USAGE)); + } + + } +} diff --git a/src/main/java/seedu/address/logic/parser/epiggy/SetGoalCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/SetGoalCommandParser.java new file mode 100644 index 000000000000..53377f0283ee --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/SetGoalCommandParser.java @@ -0,0 +1,56 @@ +package seedu.address.logic.parser.epiggy; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.epiggy.SetGoalCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.epiggy.Goal; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Name; + +//@@author kev-inc + +/** + * Parses input arguments and creates a new SetGoalCommand object + */ +public class SetGoalCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SetGoalCommand + * and returns a SetGoalCommand object for execution. + */ + @Override + public SetGoalCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_COST); + + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_COST) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SetGoalCommand.MESSAGE_USAGE)); + } + + Name name = ParserUtil.parseItemName(argMultimap.getValue(PREFIX_NAME).get()); + Cost cost = ParserUtil.parseCost(argMultimap.getValue(PREFIX_COST).get()); + + Goal goal = new Goal(name, cost); + + return new SetGoalCommand(goal); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/epiggy/SortCommandParser.java b/src/main/java/seedu/address/logic/parser/epiggy/SortCommandParser.java new file mode 100644 index 000000000000..8f9e63b4e03e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/epiggy/SortCommandParser.java @@ -0,0 +1,87 @@ +package seedu.address.logic.parser.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_COST; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.ParserUtil.validateKeywordsForSort; + +import java.util.Comparator; +import java.util.stream.Stream; + +import seedu.address.logic.commands.epiggy.SortCommand; +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.ArgumentTokenizer; +import seedu.address.logic.parser.Parser; +import seedu.address.logic.parser.Prefix; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.comparators.CompareExpenseByCost; +import seedu.address.model.epiggy.comparators.CompareExpenseByDate; +import seedu.address.model.epiggy.comparators.CompareExpenseByName; + +//@@author rahulb99 +/** + * Parses input arguments and creates a new SortCommand object. + */ +public class SortCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SortCommand + * and returns an SortCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SortCommand parse(String args) throws ParseException { + requireNonNull(args); + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + + //Check whether the user follow the pattern + if (!trimmedArgs.contains("/")) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + + String[] splitTrimmedArgs = trimmedArgs.split("/"); + if (splitTrimmedArgs[0].equals("")) { + //Ensure args contains at least one prefix + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + if (!(splitTrimmedArgs.length == 1)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + + ArgumentMultimap keywordsMap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_COST, PREFIX_DATE); + validateKeywordsForSort(keywordsMap); + Comparator comparator = null; + + if (arePrefixesPresent(keywordsMap, PREFIX_NAME)) { + comparator = new CompareExpenseByName(); + } else if (arePrefixesPresent(keywordsMap, PREFIX_DATE)) { + comparator = new CompareExpenseByDate(); + } else if (arePrefixesPresent(keywordsMap, PREFIX_COST)) { + comparator = new CompareExpenseByCost(); + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + + return new SortCommand(comparator); + } + + /** + * 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/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java deleted file mode 100644 index 30557cf81ee7..000000000000 --- a/src/main/java/seedu/address/model/AddressBook.java +++ /dev/null @@ -1,144 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import javafx.beans.InvalidationListener; -import javafx.collections.ObservableList; -import seedu.address.commons.util.InvalidationListenerManager; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; - -/** - * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) - */ -public class AddressBook implements ReadOnlyAddressBook { - - private final UniquePersonList persons; - private final InvalidationListenerManager invalidationListenerManager = new InvalidationListenerManager(); - - /* - * The 'unusual' code block below is an non-static initialization block, sometimes used to avoid duplication - * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html - * - * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication - * among constructors. - */ - { - persons = new UniquePersonList(); - } - - public AddressBook() {} - - /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} - */ - public AddressBook(ReadOnlyAddressBook toBeCopied) { - this(); - resetData(toBeCopied); - } - - //// list overwrite operations - - /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - this.persons.setPersons(persons); - indicateModified(); - } - - /** - * Resets the existing data of this {@code AddressBook} with {@code newData}. - */ - public void resetData(ReadOnlyAddressBook newData) { - requireNonNull(newData); - - setPersons(newData.getPersonList()); - } - - //// person-level operations - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); - } - - /** - * Adds a person to the address book. - * The person must not already exist in the address book. - */ - public void addPerson(Person p) { - persons.add(p); - indicateModified(); - } - - /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); - - persons.setPerson(target, editedPerson); - indicateModified(); - } - - /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. - */ - public void removePerson(Person key) { - persons.remove(key); - indicateModified(); - } - - @Override - public void addListener(InvalidationListener listener) { - invalidationListenerManager.addListener(listener); - } - - @Override - public void removeListener(InvalidationListener listener) { - invalidationListenerManager.removeListener(listener); - } - - /** - * Notifies listeners that the address book has been modified. - */ - protected void indicateModified() { - invalidationListenerManager.callListeners(this); - } - - //// util methods - - @Override - public String toString() { - return persons.asUnmodifiableObservableList().size() + " persons"; - // TODO: refine later - } - - @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddressBook // instanceof handles nulls - && persons.equals(((AddressBook) other).persons)); - } - - @Override - public int hashCode() { - return persons.hashCode(); - } -} diff --git a/src/main/java/seedu/address/model/EPiggy.java b/src/main/java/seedu/address/model/EPiggy.java new file mode 100644 index 000000000000..b0c20ef6c4ba --- /dev/null +++ b/src/main/java/seedu/address/model/EPiggy.java @@ -0,0 +1,405 @@ +package seedu.address.model; + +import static java.util.Objects.requireNonNull; + +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import javafx.beans.InvalidationListener; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; +import seedu.address.commons.util.InvalidationListenerManager; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.Budget; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.ExpenseList; +import seedu.address.model.epiggy.Goal; +import seedu.address.model.epiggy.UniqueBudgetList; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Item; +import seedu.address.model.epiggy.item.Period; +import seedu.address.model.person.Person; +import seedu.address.model.person.UniquePersonList; + +/** + * Wraps all data at the address-book level + * Duplicates are not allowed (by .isSamePerson comparison) + */ +public class EPiggy implements ReadOnlyEPiggy { + + private final ExpenseList expenses; + private final ObservableList items; + private SimpleObjectProperty goal; + private final UniquePersonList persons; + private final UniqueBudgetList budgetList; + private final InvalidationListenerManager invalidationListenerManager = new InvalidationListenerManager(); + + /* + * The 'unusual' code block below is an 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. + */ + { + expenses = new ExpenseList(); + items = FXCollections.observableArrayList(); + budgetList = new UniqueBudgetList(); + persons = new UniquePersonList(); + goal = new SimpleObjectProperty<>(); + + } + + public EPiggy() {} + + /** + * Creates an EPiggy using the Persons in the {@code toBeCopied} + */ + public EPiggy(ReadOnlyEPiggy toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the person list with {@code persons}. + * {@code persons} must not contain duplicate persons. + */ + public void setPersons(List persons) { + this.persons.setPersons(persons); + indicateModified(); + } + + //@@author tehwenyi + /** + * Replaces the contents of the person list with {@code persons}. + * {@code persons} must not contain duplicate persons. + */ + public void addBudgetList(List budgets) { + this.budgetList.addBudgetList(budgets.stream().map(Budget::new).collect(Collectors.toList())); + indicateModified(); + } + + /** + * Replaces the contents of the expense list with {@code expenses}. + * {@code expenses} can contain duplicate expenses. + */ + public void setExpenses(List expenses) { + this.expenses.setExpenses(expenses); + indicateModified(); + } + + /** + * Resets the existing data of this {@code EPiggy} with {@code newData}. + */ + public void resetData(ReadOnlyEPiggy newData) { + requireNonNull(newData); + setExpenses(newData.getExpenseList()); + setGoal(newData.getGoal().get()); + addBudgetList(newData.getBudgetList()); + } + + //// person-level operations + + /** + * Returns true if a person with the same identity as {@code person} exists in the address book. + */ + public boolean hasPerson(Person person) { + requireNonNull(person); + return persons.contains(person); + } + + /** + * Adds a person to the address book. + * The person must not already exist in the address book. + */ + public void addPerson(Person p) { + persons.add(p); + indicateModified(); + } + + /** + * Adds an expense to the expense book. + */ + public void addExpense(Expense expense) { + expenses.add(expense); + if (budgetList.getBudgetListSize() > 0) { + updateBudgetList(expense); + } + indicateModified(); + } + + //@@author tehwenyi + /** + * Updates the budgetList. Called every time an expense is added, edited or deleted. + */ + private void updateBudgetList(Expense expense) { + int indexOfBudgetToEdit = budgetList.getBudgetIndexBasedOnDate(expense.getDate()); + if (indexOfBudgetToEdit >= 0) { + Budget budgetToEdit = budgetList.getBudgetAtIndex(indexOfBudgetToEdit); + Budget editedBudget = updateBudget(budgetToEdit); + budgetList.replaceAtIndex(indexOfBudgetToEdit, editedBudget); + } + } + + /** + * Adds an allowance to the expense book. + * @param allowance to be added. + */ + public void addAllowance(Allowance allowance) { + expenses.add(allowance); + indicateModified(); + } + + public SimpleObjectProperty getSavings() { + return new SimpleObjectProperty<>(new Cost(expenses.getTotalSavings())); + } + + //@@author tehwenyi + /** + * Adds a budget to the budgetList. + * Called by the Command addBudget only. + * @param budget to be added into budgetList. + */ + public void addBudget(int index, Budget budget) { + budget = updateBudget(budget); + + budgetList.addAtIndex(index, budget); + indicateModified(); + } + + //@@author tehwenyi + /** + * Deletes the budget at the specific index. + * @param index of the to be deleted budget. + */ + public void deleteBudgetAtIndex(int index) { + budgetList.remove(index); + indicateModified(); + } + + /** + * Deletes the expense {@code toDelete}. + * @param toDelete the expense to be deleted. + */ + public void deleteExpense(Expense toDelete) { + expenses.remove(toDelete); + updateBudgetList(toDelete); + indicateModified(); + } + + //@@author tehwenyi + /** + * Updates the remaining amount and days of the budget. + * Allowances in the Expense list does not affect the budget. + * @param budget to be updated. + * @return updated budget. + */ + private Budget updateBudget(Budget budget) { + budget.setRemainingDays(calculateRemainingDays(budget)); + + budget.resetRemainingAmount(); + SortedList sortedExpensesByDate = sortExpensesByDate(); + ListIterator iterator = sortedExpensesByDate.listIterator(); + for (Expense expense : sortedExpensesByDate) { + if (!expense.getDate().before(budget.getStartDate())) { + if (budget.getEndDate().after(expense.getDate())) { + if (!(expense instanceof Allowance)) { + budget.deductRemainingAmount(expense.getItem().getCost()); + } + } else { + return budget; + } + } + } + return budget; + } + + //@@author tehwenyi + /** + * Calculates the remaining days for the budget based on the current date. + * @param budget to calculate the remaining days for. + * @return remaining days. + */ + private Period calculateRemainingDays(Budget budget) { + Date todaysDate = new Date(); + if (todaysDate.after(budget.getEndDate())) { + return new Period(0); + } + long diffInMillies = budget.getEndDate().getTime() - todaysDate.getTime(); + long diff = TimeUnit.DAYS.convert(diffInMillies, TimeUnit.MILLISECONDS); + return new Period((int) Math.ceil(diff)); + } + + //@@author tehwenyi + /** + * Checks if there are any overlapping budgets. + */ + public boolean budgetsOverlap(Date startDate, Date endDate, Budget earlierBudget) { + if (!startDate.after(earlierBudget.getStartDate()) && endDate.after(earlierBudget.getStartDate())) { + return true; + } + if (!startDate.before(earlierBudget.getStartDate()) && !endDate.after(earlierBudget.getEndDate())) { + return true; + } + if (startDate.before(earlierBudget.getEndDate()) && !endDate.before(earlierBudget.getEndDate())) { + return true; + } + return false; + } + + /** + * Sorts Expenses according to Date. Earlier Dates will have lower indexes. + * @return SortedList of Expenses + */ + public SortedList sortExpensesByDate() { + return expenses.sortByDate(); + } + + public void sortExpense(Comparator comparator) { + expenses.sort(comparator); + } + + /** + * Replaces the given expense {@code target} in the list with {@code editedExpense}. + * {@code target} must exist in the expense tracker. + * The expense identity of {@code editedExpense} + * must not be the same as another existing expense in the expense tracker. + */ + public void setExpense(Expense target, Expense editedExpense) { + requireNonNull(editedExpense); + expenses.setExpense(target, editedExpense); + updateBudgetList(editedExpense); + updateBudgetList(target); + indicateModified(); + } + + //@@author tehwenyi + /** + * Replaces the current/previous budget in the list with {@code editedBudget}. + * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + */ + public void setCurrentBudget(Budget editedBudget) { + requireNonNull(editedBudget); + int indexOfCurrentBudget = budgetList.getCurrentBudgetIndex(); + budgetList.replaceAtIndex(indexOfCurrentBudget, updateBudget(editedBudget)); + indicateModified(); + } + + //@@author tehwenyi + /** + * Gets the current budget's index. + * @return -1 if there is no current budget. + */ + public int getCurrentBudgetIndex() { + return this.budgetList.getCurrentBudgetIndex(); + }; + + /** + * Sets the saving goal for ePiggy. + */ + public void setGoal(Goal goal) { + this.goal.setValue(goal); + indicateModified(); + } + + /** + * Get the saving goal for ePiggy. + */ + public SimpleObjectProperty getGoal() { + return goal; + } + + /** + * Replaces the given person {@code target} in the list with {@code editedPerson}. + * {@code target} must exist in the address book. + * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + */ + public void setPerson(Person target, Person editedPerson) { + requireNonNull(editedPerson); + + persons.setPerson(target, editedPerson); + indicateModified(); + } + + /** + * Removes {@code key} from this {@code EPiggy}. + * {@code key} must exist in the address book. + */ + public void removePerson(Person key) { + persons.remove(key); + indicateModified(); + } + + @Override + public void addListener(InvalidationListener listener) { + invalidationListenerManager.addListener(listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + invalidationListenerManager.removeListener(listener); + } + + /** + * Notifies listeners that the address book has been modified. + */ + protected void indicateModified() { + invalidationListenerManager.callListeners(this); + } + + //// util methods + + @Override + public String toString() { + return expenses.asUnmodifiableObservableList() + " expenselist\n" + + budgetList.asUnmodifiableObservableList() + " budgets"; + } + + @Override + public ObservableList getPersonList() { + return persons.asUnmodifiableObservableList(); + } + + @Override + public ObservableList getExpenseList() { + return expenses.asUnmodifiableObservableList(); + } + + @Override + public ObservableList getItemList() { + return FXCollections.unmodifiableObservableList(items); + } + + //@@author tehwenyi + /** + * Gets the current budget list for ePiggy. + */ + @Override + public ObservableList getBudgetList() { + return budgetList.asUnmodifiableObservableList(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EPiggy // instanceof handles nulls + && expenses.equals(((EPiggy) other).expenses)); + } + + @Override + public int hashCode() { + return persons.hashCode(); + } + + public void reverseExpenseList() { + expenses.reverse(); + } +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index e857533821b6..ea698cff4700 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,11 +1,19 @@ package seedu.address.model; import java.nio.file.Path; +import java.util.Comparator; +import java.util.Date; import java.util.function.Predicate; import javafx.beans.property.ReadOnlyProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.Budget; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.Goal; +import seedu.address.model.epiggy.item.Cost; import seedu.address.model.person.Person; /** @@ -15,6 +23,12 @@ public interface Model { /** {@code Predicate} that always evaluate to true */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_BUDGETS = unused -> true; + + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_EXPENSES = unused -> true; + /** * Replaces user prefs data with the data in {@code userPrefs}. */ @@ -38,20 +52,20 @@ public interface Model { /** * Returns the user prefs' address book file path. */ - Path getAddressBookFilePath(); + Path getEPiggyFilePath(); /** * Sets the user prefs' address book file path. */ - void setAddressBookFilePath(Path addressBookFilePath); + void setEPiggyFilePath(Path addressBookFilePath); /** - * Replaces address book data with the data in {@code addressBook}. + * Replaces address book data with the data in {@code ePiggy}. */ - void setAddressBook(ReadOnlyAddressBook addressBook); + void setEPiggy(ReadOnlyEPiggy ePiggy); - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); + /** Returns the EPiggy */ + ReadOnlyEPiggy getEPiggy(); /** * Returns true if a person with the same identity as {@code person} exists in the address book. @@ -70,6 +84,84 @@ public interface Model { */ void addPerson(Person person); + /** + * Adds the given expense. + * {@code person} must not already exist in the address book. + */ + void addExpense(Expense expense); + + /** + * Adds the given allowance. + */ + void addAllowance(Allowance allowance); + + /** + * Adds a new budget. + */ + void addBudget(int index, Budget budget); + + /** + * Checks if there are any overlapping budgets. + */ + boolean budgetsOverlap(Date startDate, Date endDate, Budget earlierBudget); + + /** + * Deletes the budget at the specific index. + */ + void deleteBudgetAtIndex(int index); + + /** + * Deletes the expense {@code toDelete}. + * @param toDelete the expense to be deleted. + */ + void deleteExpense(Expense toDelete); + + /** + * Replaces the given expense {@code target} with {@code editedExpense}. + * {@code target} must exist in the address book. + * The expense identity of {@code editedExpense} must not be the same as + * another existing expense in the address book. + */ + void setExpense(Expense target, Expense editedExpense); + + /** + * Updates the filter of the filtered expense list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredExpensesList(Predicate predicate); + + /** + * Gets the current budget list. + */ + ObservableList getBudgetList(); + + /** + * Gets the Expense list. + */ + ObservableList getExpenseList(); + + /** + * Gets the current budget's index. + * @return -1 if there is no current budget. + */ + int getCurrentBudgetIndex(); + + + /** + * Get the current savings. + */ + SimpleObjectProperty getSavings(); + + /** + * Get the savings goal. + */ + SimpleObjectProperty getGoal(); + + /** + * Sets the savings goal. + */ + void setGoal(Goal goal); + /** * Replaces the given person {@code target} with {@code editedPerson}. * {@code target} must exist in the address book. @@ -77,39 +169,56 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); + /** + * Replaces the current budget with {@code editedBudget}. + */ + void setCurrentBudget(Budget editedBudget); + /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered expense list */ + ObservableList getFilteredExpenseList(); + + /** Returns an unmodifiable view of the filtered budget list */ + ObservableList getFilteredBudgetList(); + /** * 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); + /** + * Updates the filter of the filtered budget list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredBudgetList(Predicate predicate); + /** * Returns true if the model has previous address book states to restore. */ - boolean canUndoAddressBook(); + boolean canUndoEPiggy(); /** * Returns true if the model has undone address book states to restore. */ - boolean canRedoAddressBook(); + boolean canRedoEPiggy(); /** * Restores the model's address book to its previous state. */ - void undoAddressBook(); + void undoEPiggy(); /** * Restores the model's address book to its previously undone state. */ - void redoAddressBook(); + void redoEPiggy(); /** * Saves the current address book state for undo/redo. */ - void commitAddressBook(); + void commitEPiggy(); /** * Selected person in the filtered person list. @@ -117,14 +226,35 @@ public interface Model { */ ReadOnlyProperty selectedPersonProperty(); + /** + * Selected person in the filtered person list. + * null if no person is selected. + */ + ReadOnlyProperty selectedExpenseProperty(); + /** * Returns the selected person in the filtered person list. * null if no person is selected. */ - Person getSelectedPerson(); + Expense getSelectedExpense(); /** * Sets the selected person in the filtered person list. */ void setSelectedPerson(Person person); + + /** + * Sets the selected expense in the filtered expense list. + */ + void setSelectedExpense(Expense expense); + + /** + * Sorts the expenses according to the specified {@param expenseComparator}. + */ + void sortExpenses(Comparator expenseComparator); + + /** + * Reveres the {@code filteredExpenses} list. + */ + void reverseFilteredExpensesList(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index b56806232814..a0d202216dd8 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,6 +4,8 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Comparator; +import java.util.Date; import java.util.Objects; import java.util.function.Predicate; import java.util.logging.Logger; @@ -15,6 +17,11 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.Budget; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.Goal; +import seedu.address.model.epiggy.item.Cost; import seedu.address.model.person.Person; import seedu.address.model.person.exceptions.PersonNotFoundException; @@ -24,28 +31,34 @@ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - private final VersionedAddressBook versionedAddressBook; + private final VersionedEPiggy versionedEPiggy; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final FilteredList filteredExpenses; + private final FilteredList filteredBudget; private final SimpleObjectProperty selectedPerson = new SimpleObjectProperty<>(); + private final SimpleObjectProperty selectedExpense = new SimpleObjectProperty<>(); /** - * Initializes a ModelManager with the given addressBook and userPrefs. + * Initializes a ModelManager with the given ePiggy and userPrefs. */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { + public ModelManager(ReadOnlyEPiggy ePiggy, ReadOnlyUserPrefs userPrefs) { super(); - requireAllNonNull(addressBook, userPrefs); + requireAllNonNull(ePiggy, userPrefs); - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); + logger.fine("Initializing with address book: " + ePiggy + " and user prefs " + userPrefs); - versionedAddressBook = new VersionedAddressBook(addressBook); + versionedEPiggy = new VersionedEPiggy(ePiggy); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(versionedAddressBook.getPersonList()); + filteredPersons = new FilteredList<>(versionedEPiggy.getPersonList()); filteredPersons.addListener(this::ensureSelectedPersonIsValid); + + filteredExpenses = new FilteredList<>(versionedEPiggy.getExpenseList()); + filteredBudget = new FilteredList<>(versionedEPiggy.getBudgetList()); } public ModelManager() { - this(new AddressBook(), new UserPrefs()); + this(new EPiggy(), new UserPrefs()); } //=========== UserPrefs ================================================================================== @@ -73,94 +86,232 @@ public void setGuiSettings(GuiSettings guiSettings) { } @Override - public Path getAddressBookFilePath() { - return userPrefs.getAddressBookFilePath(); + public Path getEPiggyFilePath() { + return userPrefs.getEPiggyFilePath(); } @Override - public void setAddressBookFilePath(Path addressBookFilePath) { + public void setEPiggyFilePath(Path addressBookFilePath) { requireNonNull(addressBookFilePath); - userPrefs.setAddressBookFilePath(addressBookFilePath); + userPrefs.setEPiggyFilePath(addressBookFilePath); } - //=========== AddressBook ================================================================================ + //=========== EPiggy ================================================================================ + + @Override + public void setEPiggy(ReadOnlyEPiggy ePiggy) { + versionedEPiggy.resetData(ePiggy); + } @Override - public void setAddressBook(ReadOnlyAddressBook addressBook) { - versionedAddressBook.resetData(addressBook); + public ReadOnlyEPiggy getEPiggy() { + return versionedEPiggy; } @Override - public ReadOnlyAddressBook getAddressBook() { - return versionedAddressBook; + public void setExpense(seedu.address.model.epiggy.Expense target, + seedu.address.model.epiggy.Expense editedExpense) { + requireAllNonNull(target, editedExpense); + + versionedEPiggy.setExpense(target, editedExpense); + setSelectedExpense(editedExpense); } @Override public boolean hasPerson(Person person) { requireNonNull(person); - return versionedAddressBook.hasPerson(person); + return versionedEPiggy.hasPerson(person); } @Override public void deletePerson(Person target) { - versionedAddressBook.removePerson(target); + versionedEPiggy.removePerson(target); } @Override public void addPerson(Person person) { - versionedAddressBook.addPerson(person); + versionedEPiggy.addPerson(person); updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); } + @Override + public void addExpense(Expense expense) { + versionedEPiggy.addExpense(expense); + setSelectedExpense(expense); + } + + @Override + public void addAllowance(Allowance allowance) { + versionedEPiggy.addAllowance(allowance); + setSelectedExpense(allowance); + } + + //@@author tehwenyi + @Override + public void addBudget(int index, Budget budget) { + versionedEPiggy.addBudget(index, budget); } + + //@@author tehwenyi + /** + * Checks if there are any overlapping budgets. + */ + public boolean budgetsOverlap(Date startDate, Date endDate, Budget earlierBudget) { + return versionedEPiggy.budgetsOverlap(startDate, endDate, earlierBudget); + } + + //@@author tehwenyi + @Override + public void deleteBudgetAtIndex(int index) { + versionedEPiggy.deleteBudgetAtIndex(index); + } + + @Override + public void deleteExpense(Expense toDelete) { + versionedEPiggy.deleteExpense(toDelete); + if (selectedExpenseProperty().getValue() == toDelete) { + setSelectedExpense(null); + } + } + + //@@author tehwenyi + @Override + public ObservableList getBudgetList() { + return versionedEPiggy.getBudgetList(); + } + + @Override + public ObservableList getExpenseList() { + return versionedEPiggy.getExpenseList(); + } + + //@@author tehwenyi + @Override + public int getCurrentBudgetIndex() { + return versionedEPiggy.getCurrentBudgetIndex(); + } + + @Override + public SimpleObjectProperty getSavings() { + return versionedEPiggy.getSavings(); + } + + @Override + public SimpleObjectProperty getGoal() { + return versionedEPiggy.getGoal(); + } + + @Override + public void setGoal(Goal goal) { + versionedEPiggy.setGoal(goal); + } + @Override public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); - versionedAddressBook.setPerson(target, editedPerson); + versionedEPiggy.setPerson(target, editedPerson); + } + + //@@author tehwenyi + @Override + public void setCurrentBudget(Budget editedBudget) { + requireNonNull(editedBudget); + + versionedEPiggy.setCurrentBudget(editedBudget); } //=========== Filtered Person List Accessors ============================================================= /** * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of - * {@code versionedAddressBook} + * {@code versionedEPiggy} */ @Override public ObservableList getFilteredPersonList() { return filteredPersons; } + /** + * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of + * {@code versionedEPiggy} + */ + @Override + public ObservableList getFilteredExpenseList() { + return filteredExpenses; + } + + /** + * Returns an unmodifiable view of the list of {@code Budget} backed by the internal list of + * {@code versionedEPiggy} + */ + @Override + public ObservableList getFilteredBudgetList() { + return filteredBudget; + } + @Override public void updateFilteredPersonList(Predicate predicate) { requireNonNull(predicate); filteredPersons.setPredicate(predicate); } + //@@author rahulb99 + @Override + public void updateFilteredExpensesList(Predicate predicate) { + requireNonNull(predicate); + filteredExpenses.setPredicate(predicate); + } + + //@@author rahulb99 + /** + * Sorts the expenses according to the keyword. + * @param comparator expense comparator + */ + public void sortExpenses(Comparator comparator) { + requireAllNonNull(comparator); + versionedEPiggy.sortExpense(comparator); + } + + @Override + public void updateFilteredBudgetList(Predicate predicate) { + requireNonNull(predicate); + filteredBudget.setPredicate(predicate); + } + + //@@author rahulb99 + + /** + * Reveres the {@code filteredExpenses} list. + */ + public void reverseFilteredExpensesList() { + versionedEPiggy.reverseExpenseList(); + } + //=========== Undo/Redo ================================================================================= @Override - public boolean canUndoAddressBook() { - return versionedAddressBook.canUndo(); + public boolean canUndoEPiggy() { + return versionedEPiggy.canUndo(); } @Override - public boolean canRedoAddressBook() { - return versionedAddressBook.canRedo(); + public boolean canRedoEPiggy() { + return versionedEPiggy.canRedo(); } @Override - public void undoAddressBook() { - versionedAddressBook.undo(); + public void undoEPiggy() { + versionedEPiggy.undo(); } @Override - public void redoAddressBook() { - versionedAddressBook.redo(); + public void redoEPiggy() { + versionedEPiggy.redo(); } @Override - public void commitAddressBook() { - versionedAddressBook.commit(); + public void commitEPiggy() { + versionedEPiggy.commit(); } //=========== Selected person =========================================================================== @@ -171,8 +322,13 @@ public ReadOnlyProperty selectedPersonProperty() { } @Override - public Person getSelectedPerson() { - return selectedPerson.getValue(); + public ReadOnlyProperty selectedExpenseProperty() { + return selectedExpense; + } + + @Override + public Expense getSelectedExpense() { + return selectedExpense.getValue(); } @Override @@ -183,6 +339,14 @@ public void setSelectedPerson(Person person) { selectedPerson.setValue(person); } + @Override + public void setSelectedExpense(Expense expense) { + if (expense != null && !filteredExpenses.contains(expense)) { + throw new PersonNotFoundException(); + } + selectedExpense.setValue(expense); + } + /** * Ensures {@code selectedPerson} is a valid person in {@code filteredPersons}. */ @@ -226,10 +390,22 @@ public boolean equals(Object obj) { // state check ModelManager other = (ModelManager) obj; - return versionedAddressBook.equals(other.versionedAddressBook) + return versionedEPiggy.equals(other.versionedEPiggy) && userPrefs.equals(other.userPrefs) && filteredPersons.equals(other.filteredPersons) && Objects.equals(selectedPerson.get(), other.selectedPerson.get()); } + @Override + public String toString() { + return "ModelManager{" + + "versionedAddressBook=" + versionedEPiggy + + ", userPrefs=" + userPrefs + + ", filteredPersons=" + filteredPersons + + ", filteredExpenses=" + filteredExpenses + + ", filteredBudget=" + filteredBudget + + ", selectedPerson=" + selectedPerson + + ", selectedExpense=" + selectedExpense + + '}'; + } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java deleted file mode 100644 index 6a301434b33b..000000000000 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ /dev/null @@ -1,18 +0,0 @@ -package seedu.address.model; - -import javafx.beans.Observable; -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; - -/** - * Unmodifiable view of an address book - */ -public interface ReadOnlyAddressBook extends Observable { - - /** - * 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/ReadOnlyEPiggy.java b/src/main/java/seedu/address/model/ReadOnlyEPiggy.java new file mode 100644 index 000000000000..9079749d38d7 --- /dev/null +++ b/src/main/java/seedu/address/model/ReadOnlyEPiggy.java @@ -0,0 +1,43 @@ +package seedu.address.model; + +import javafx.beans.Observable; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; +import seedu.address.model.epiggy.Budget; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.Goal; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Item; +import seedu.address.model.person.Person; + +/** + * Unmodifiable view of an address book + */ +public interface ReadOnlyEPiggy extends Observable { + + /** + * Returns an unmodifiable view of the persons list. + * This list will not contain any duplicate persons. + */ + ObservableList getPersonList(); + + /** + * Returns an unmodifiable view of the expense list. + */ + ObservableList getExpenseList(); + + /** + * Returns an unmodifiable view of the item list. + */ + ObservableList getItemList(); + + /** + * Returns an unmodifiable view of the budget list. + */ + ObservableList getBudgetList(); + + ObservableValue getSavings(); + + SimpleObjectProperty getGoal(); +} diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java index befd58a4c739..6a64302236a3 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java @@ -11,6 +11,6 @@ public interface ReadOnlyUserPrefs { GuiSettings getGuiSettings(); - Path getAddressBookFilePath(); + Path getEPiggyFilePath(); } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 25a5fd6eab9e..c64fc53b9d76 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -14,7 +14,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path addressBookFilePath = Paths.get("data" , "epiggy.json"); /** * Creates a {@code UserPrefs} with default values. @@ -35,7 +35,7 @@ public UserPrefs(ReadOnlyUserPrefs userPrefs) { public void resetData(ReadOnlyUserPrefs newUserPrefs) { requireNonNull(newUserPrefs); setGuiSettings(newUserPrefs.getGuiSettings()); - setAddressBookFilePath(newUserPrefs.getAddressBookFilePath()); + setEPiggyFilePath(newUserPrefs.getEPiggyFilePath()); } public GuiSettings getGuiSettings() { @@ -47,11 +47,11 @@ public void setGuiSettings(GuiSettings guiSettings) { this.guiSettings = guiSettings; } - public Path getAddressBookFilePath() { + public Path getEPiggyFilePath() { return addressBookFilePath; } - public void setAddressBookFilePath(Path addressBookFilePath) { + public void setEPiggyFilePath(Path addressBookFilePath) { requireNonNull(addressBookFilePath); this.addressBookFilePath = addressBookFilePath; } diff --git a/src/main/java/seedu/address/model/VersionedAddressBook.java b/src/main/java/seedu/address/model/VersionedEPiggy.java similarity index 74% rename from src/main/java/seedu/address/model/VersionedAddressBook.java rename to src/main/java/seedu/address/model/VersionedEPiggy.java index e17a9e3ba4ab..b0b135522b9f 100644 --- a/src/main/java/seedu/address/model/VersionedAddressBook.java +++ b/src/main/java/seedu/address/model/VersionedEPiggy.java @@ -4,28 +4,28 @@ import java.util.List; /** - * {@code AddressBook} that keeps track of its own history. + * {@code EPiggy} that keeps track of its own history. */ -public class VersionedAddressBook extends AddressBook { +public class VersionedEPiggy extends EPiggy { - private final List addressBookStateList; + private final List addressBookStateList; private int currentStatePointer; - public VersionedAddressBook(ReadOnlyAddressBook initialState) { + public VersionedEPiggy(ReadOnlyEPiggy initialState) { super(initialState); addressBookStateList = new ArrayList<>(); - addressBookStateList.add(new AddressBook(initialState)); + addressBookStateList.add(new EPiggy(initialState)); currentStatePointer = 0; } /** - * Saves a copy of the current {@code AddressBook} state at the end of the state list. + * Saves a copy of the current {@code EPiggy} state at the end of the state list. * Undone states are removed from the state list. */ public void commit() { removeStatesAfterCurrentPointer(); - addressBookStateList.add(new AddressBook(this)); + addressBookStateList.add(new EPiggy(this)); currentStatePointer++; indicateModified(); } @@ -78,16 +78,16 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof VersionedAddressBook)) { + if (!(other instanceof VersionedEPiggy)) { return false; } - VersionedAddressBook otherVersionedAddressBook = (VersionedAddressBook) other; + VersionedEPiggy otherVersionedEPiggyBook = (VersionedEPiggy) other; // state check - return super.equals(otherVersionedAddressBook) - && addressBookStateList.equals(otherVersionedAddressBook.addressBookStateList) - && currentStatePointer == otherVersionedAddressBook.currentStatePointer; + return super.equals(otherVersionedEPiggyBook) + && addressBookStateList.equals(otherVersionedEPiggyBook.addressBookStateList) + && currentStatePointer == otherVersionedEPiggyBook.currentStatePointer; } /** diff --git a/src/main/java/seedu/address/model/epiggy/Allowance.java b/src/main/java/seedu/address/model/epiggy/Allowance.java new file mode 100644 index 000000000000..f3bcd1a642b5 --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/Allowance.java @@ -0,0 +1,31 @@ +package seedu.address.model.epiggy; + +import java.util.Date; + +import seedu.address.model.epiggy.item.Item; + +//@@author kev-inc + +/** + * Represents an allowance in the epiggy. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Allowance extends Expense { + public Allowance(Item item, Date date) { + super(item, date); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Allowance)) { + return false; + } + Allowance otherAllowance = (Allowance) other; + return otherAllowance.getItem().equals(getItem()) + && otherAllowance.getDate().equals(getDate()); + } + +} diff --git a/src/main/java/seedu/address/model/epiggy/Budget.java b/src/main/java/seedu/address/model/epiggy/Budget.java new file mode 100644 index 000000000000..1367cf0c7a26 --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/Budget.java @@ -0,0 +1,183 @@ +package seedu.address.model.epiggy; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Period; + +//@@author tehwenyi + +/** + * Represents a Budget in ePiggy. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Budget { + public static final String CURRENT_BUDGET = "Current"; + private static final String OLD_BUDGET = "Old"; + private static final String FUTURE_BUDGET = "Future"; + + private final Cost amount; + private final Date startDate; + private final Date endDate; + private final Period period; + + private Cost remainingAmount; + private Period remainingDays; + private String status = null; + private Date todaysDate; + + /** + * Represents a Budget in the expense book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ + public Budget(Cost amount, Period period, Date startDate) { + this.amount = amount; + this.startDate = startDate; + this.period = period; + this.endDate = calculateEndDate(startDate, period); + this.remainingAmount = amount; + this.remainingDays = period; + this.todaysDate = new Date(); + if (!todaysDate.before(startDate) && !todaysDate.after(endDate)) { + this.status = CURRENT_BUDGET; + } else if (todaysDate.before(startDate)) { + this.status = FUTURE_BUDGET; + } else { + this.status = OLD_BUDGET; + } + } + + /** + * Clones budget. + */ + public Budget(Budget budget) { + this.amount = budget.amount; + this.startDate = budget.startDate; + this.period = budget.period; + this.endDate = budget.endDate; + this.remainingAmount = budget.remainingAmount; + this.remainingDays = budget.remainingDays; + this.todaysDate = budget.todaysDate; + this.status = budget.status; + } + + /** + * Calculates the end date = startDate + period (number of days) + * @param startDate + * @param period + * @return endDate + */ + private Date calculateEndDate(Date startDate, Period period) { + Calendar cal = Calendar.getInstance(); + cal.setTime(this.startDate); + cal.add(Calendar.DATE, period.getTimePeriod()); + return cal.getTime(); + } + + public void setRemainingDays(Period remainingDays) { + this.remainingDays = remainingDays; + } + + public void setRemainingAmount(Cost remainingAmount) { + this.remainingAmount = remainingAmount; + } + + public void resetRemainingAmount() { + this.remainingAmount = this.amount; + } + + public void deductRemainingAmount(Cost amountToDeduct) { + this.remainingAmount = this.remainingAmount.deduct(amountToDeduct); + } + + public String getStatus() { + return status; + } + + public Cost getRemainingAmount() { + return remainingAmount; + } + + public Cost getPositiveRemainingAmount() { + if (remainingAmount.getAmount() < 0) { + return new Cost(-remainingAmount.getAmount()); + } + return remainingAmount; + } + + public Period getRemainingDays() { + return remainingDays; } + + public Cost getBudgetedAmount() { + return this.amount; + } + + public Period getPeriod() { + return this.period; + } + + public Date getStartDate() { + return this.startDate; + } + + public Date getEndDate() { + return this.endDate; + } + + public int getDay() { + Calendar cal = Calendar.getInstance(); + cal.setTime(this.startDate); + return cal.get(Calendar.DAY_OF_MONTH); + } + + public int getMonth() { + Calendar cal = Calendar.getInstance(); + cal.setTime(this.startDate); + return cal.get(Calendar.MONTH); + } + + public int getYear() { + Calendar cal = Calendar.getInstance(); + cal.setTime(this.startDate); + return cal.get(Calendar.YEAR); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof Budget)) { + return false; + } + + Budget b = (Budget) o; + Calendar cal = Calendar.getInstance(); + cal.setTime(this.startDate); + return this.amount.equals(b.getBudgetedAmount()) + && cal.get(Calendar.YEAR) == b.getYear() + && cal.get(Calendar.MONTH) == b.getMonth() + && cal.get(Calendar.DAY_OF_MONTH) == b.getDay() + && this.period.equals(b.getPeriod()); + } + + @Override + public String toString() { + Calendar cal = Calendar.getInstance(); + cal.setTime(getEndDate()); + cal.add(Calendar.DAY_OF_MONTH, -1); + SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy"); + final StringBuilder builder = new StringBuilder(); + builder.append("$") + .append(getBudgetedAmount()) + .append(" from ") + .append(sdf.format(getStartDate())) + .append(" to ") + .append(sdf.format(cal.getTime())) + .append("."); + return builder.toString(); + } +} diff --git a/src/main/java/seedu/address/model/epiggy/Expense.java b/src/main/java/seedu/address/model/epiggy/Expense.java new file mode 100644 index 000000000000..1161a20e0fb6 --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/Expense.java @@ -0,0 +1,51 @@ +package seedu.address.model.epiggy; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import seedu.address.model.epiggy.item.Item; + +/** + * Represents an Expense in the expense book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Expense { + private final Item item; + private final Date date; + + public Expense(Item item, Date date) { + this.item = item; + this.date = date; + } + + public Item getItem() { + return item; + } + + public Date getDate() { + return date; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + SimpleDateFormat format = new SimpleDateFormat("EEE, MMM d, ''yy"); + builder.append(item) + .append("\nAdded on: ") + .append(format.format(date)); + return builder.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Expense)) { + return false; + } + Expense otherExpense = (Expense) other; + return otherExpense.getItem().equals(getItem()) + && otherExpense.getDate().equals(getDate()); + } +} diff --git a/src/main/java/seedu/address/model/epiggy/ExpenseContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/epiggy/ExpenseContainsKeywordsPredicate.java new file mode 100644 index 000000000000..f0b60b9a57aa --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/ExpenseContainsKeywordsPredicate.java @@ -0,0 +1,196 @@ +package seedu.address.model.epiggy; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.logic.parser.ArgumentMultimap; +import seedu.address.logic.parser.CliSyntax; +import seedu.address.model.epiggy.item.Item; + +//@@author rahulb99 +/** + * Tests that a {@code expense}'s {@code Name, Cost, Category, Date} matches any of the keywords given. + */ +public class ExpenseContainsKeywordsPredicate implements Predicate { + + public static final int LEVENSHTIEN_THRESHOLD = 3; + private final ArgumentMultimap keywords; + + public ExpenseContainsKeywordsPredicate(ArgumentMultimap keywords) { + assert keywords != null : "keywords should not be null."; + this.keywords = keywords; + } + + /** + * Evaluates this predicate on the given argument. + * + * @param expense the input argument + * @return {@code true} if the input argument matches the predicate, + * otherwise {@code false} + */ + @Override + public boolean test(Expense expense) { + assert expense != null : "expense should not be null."; + + List nameKeywords = keywords.getAllValues(CliSyntax.PREFIX_NAME); + List tagKeywords = keywords.getAllValues(CliSyntax.PREFIX_TAG); + String dateKeywords = keywords.getValue(CliSyntax.PREFIX_DATE).orElse(""); + String costKeywords = keywords.getValue(CliSyntax.PREFIX_COST).orElse(""); + + //if all keywords are absent, return false + if (nameKeywords.isEmpty() && tagKeywords.isEmpty() + && dateKeywords.equals("") && costKeywords.equals("")) { + return false; + } + + //if one or more keywords are present + boolean result = true; + if (!nameKeywords.isEmpty()) { + result = containsNameKeywords(nameKeywords, expense); + } + + if (!costKeywords.equals("")) { + result = result && isWithinCostRange(costKeywords, expense); + } + + if (!dateKeywords.equals("")) { + try { + result = result && isWithinDateRange(dateKeywords, expense); + } catch (java.text.ParseException e) { + result = false; + e.printStackTrace(); + } + } + + if (!tagKeywords.isEmpty()) { + result = result && checkTagKeywords(tagKeywords, expense); + } + + return result; + } + + /** + * Return true if the {@code Name} of {@code expense} contains {@code nameKeywords}. + * */ + public boolean containsNameKeywords(List nameKeywords, Expense expense) { + assert nameKeywords != null : "nameKeywords should not be null.\n"; + Item item = expense.getItem(); + boolean result = true; + for (String n: nameKeywords) { + result = result && item.getName().name.toLowerCase().contains(n.trim().toLowerCase()); + result = result || (levenshtienDist(item.getName().name, n) < LEVENSHTIEN_THRESHOLD); + } + return result; + } + + /** + * Return true if any of the {@code Tag} of {@code expense} contains any element of {@code tagKeywords}. + * */ + public boolean checkTagKeywords(List tagKeywords, Expense expense) { + assert tagKeywords != null : "tagKeywords should not be null.\n"; + boolean result = true; + Item item = expense.getItem(); + for (String tag : tagKeywords) { + result = result && item.getTags().stream() + .anyMatch(keyword -> (keyword.tagName.trim().toLowerCase().contains(tag.trim().toLowerCase())) + || (levenshtienDist(keyword.tagName, tag) < LEVENSHTIEN_THRESHOLD)); + } + return result; + } + + /** + * Return true if the {@code Cost} of {@code expense} is within the range denoted by {@code costKeywords}. + * */ + public boolean isWithinCostRange(String costKeywords, Expense expense) { + assert costKeywords != null : "costKeywords should not be null.\n"; + boolean result = true; + String[] splitCost = costKeywords.split(":"); + Item item = expense.getItem(); + if (splitCost.length == 1) { //if the user enters an exact cost + double chosenCost = Double.parseDouble(splitCost[0]); + result = item.getCost().getAmount() == chosenCost; + } else { //if the user enters a range of dates + double lowerBound = Double.parseDouble(splitCost[0]); + double higherBound = Double.parseDouble(splitCost[1]); + result = lowerBound <= item.getCost().getAmount() + && item.getCost().getAmount() <= higherBound; + } + return result; + } + + /** + * Return true if the {@code Date} of {@code expense} is within the range denoted by {@code dateKeywords}. + * */ + public boolean isWithinDateRange(String dateKeywords, Expense expense) throws java.text.ParseException { + assert dateKeywords != null : "dateKeywords should not be null.\n"; + boolean result = true; + String[] splitDate = dateKeywords.split(":"); + if (splitDate.length == 1) { //if the user only enter an exact date + Calendar cal = Calendar.getInstance(); + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + cal.setTime(sdf.parse(dateKeywords)); + Calendar expenseCal = Calendar.getInstance(); + // SimpleDateFormat sdf1 = new SimpleDateFormat("EEE, MMM d, yyyy"); + // expenseCal.setTime(sdf1.parse(expense.getDate().toString())); + expenseCal.setTime(expense.getDate()); + result = cal.get(Calendar.YEAR) == expenseCal.get(Calendar.YEAR) + && cal.get(Calendar.MONTH) == expenseCal.get(Calendar.MONTH) + && cal.get(Calendar.DATE) == expenseCal.get(Calendar.DATE); + } else { //if the user enter a range of dates + Calendar start = Calendar.getInstance(); + Calendar end = Calendar.getInstance(); + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + start.setTime(sdf.parse(splitDate[0])); + end.setTime(sdf.parse(splitDate[1])); + Calendar expenseCal = Calendar.getInstance(); + expenseCal.setTime(expense.getDate()); + boolean isWithinRange = start.before(expenseCal) + && end.after(expenseCal); + boolean equalOrNot = (start.get(Calendar.YEAR) == expenseCal.get(Calendar.YEAR) + && start.get(Calendar.MONTH) == expenseCal.get(Calendar.MONTH) + && start.get(Calendar.DATE) == expenseCal.get(Calendar.DATE)) + || (end.get(Calendar.YEAR) == expenseCal.get(Calendar.YEAR) + && end.get(Calendar.MONTH) == expenseCal.get(Calendar.MONTH) + && end.get(Calendar.DATE) == expenseCal.get(Calendar.DATE)); + result = (equalOrNot || isWithinRange); + } + return result; + } + + /** + * Calculate Levenshtien distance for almost similar words. + * @param a {@code Name} + * @param b input keyword + * @return levenshtien distance + */ + public static int levenshtienDist(String a, String b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + // i == 0 + int [] costs = new int [b.length() + 1]; + for (int j = 0; j < costs.length; j++) { + costs[j] = j; + } + for (int i = 1; i <= a.length(); i++) { + // j == 0; nw = lev(i - 1, j) + costs[0] = i; + int nw = i - 1; + for (int j = 1; j <= b.length(); j++) { + int cj = Math.min(1 + Math.min(costs[j], costs[j - 1]), + a.charAt(i - 1) == b.charAt(j - 1) ? nw : nw + 1); + nw = costs[j]; + costs[j] = cj; + } + } + return costs[b.length()]; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ExpenseContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((ExpenseContainsKeywordsPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/epiggy/ExpenseList.java b/src/main/java/seedu/address/model/epiggy/ExpenseList.java new file mode 100644 index 000000000000..e315cb57c3b6 --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/ExpenseList.java @@ -0,0 +1,169 @@ +package seedu.address.model.epiggy; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; +import seedu.address.model.person.exceptions.PersonNotFoundException; + +//@@author rahulb99 + +/** + * A list of persons that enforces uniqueness between its elements and does not allow nulls. + * A expense is considered unique by comparing using {@code Expense#isSamePerson(Expense)}. As such, + * adding and updating of persons uses Expense#isSamePerson(Expense) for equality so as to + * ensure that the expense being added or updated is + * unique in terms of identity in the ExpenseList. However, the removal of a expense uses Expense#equals(Object) so + * as to ensure that the expense with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + */ +public class ExpenseList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Adds a expense to the list. + */ + public void add(Expense toAdd) { + requireNonNull(toAdd); + internalList.add(toAdd); + } + + /** + * Replaces the expense {@code target} in the list with {@code editedExpense}. + * {@code target} must exist in the list. + * The expense identity of {@code editedExpense} must not be the same as another existing expense in the list. + */ + public void setExpense(Expense target, + Expense editedExpense) { + requireAllNonNull(target, editedExpense); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new PersonNotFoundException(); + } + + internalList.set(index, editedExpense); + } + + /** + * Removes the equivalent expense from the list. + * The expense must exist in the list. + */ + public void remove(Expense toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new PersonNotFoundException(); + } + } + + /** + * Removes the expense with the specific index from the list. + * The expense of the index must exist in the list. + * @param index of the expense to be removed. + */ + public void remove(int index) { + internalList.remove(index, index + 1); + } + + + /** + * Replaces the contents of this list with {@code expenses}. + * {@code expenses} must not contain duplicate expenses. + */ + public void setExpenses(List expenses) { + requireAllNonNull(expenses); + internalList.setAll(expenses); + } + + public double getTotalExpenses() { + double sum = internalUnmodifiableList.stream() + .filter(expense -> !(expense instanceof Allowance)) + .mapToDouble(expense -> expense.getItem().getCost().getAmount()) + .sum(); + return sum; + } + + public double getTotalAllowances() { + double sum = internalUnmodifiableList.stream() + .filter(allowance -> allowance instanceof Allowance) + .mapToDouble(allowance -> allowance.getItem().getCost().getAmount()) + .sum(); + return sum; + } + + public double getTotalSavings() { + return getTotalAllowances() - getTotalExpenses(); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ExpenseList // instanceof handles nulls + && internalList.equals(((ExpenseList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * @return size of {@param internalList} + */ + public int size() { + return internalList.size(); + } + + /** + * Returns list of expenses sorted by date + * @return SortedList of Expense + */ + public SortedList sortByDate() { + return internalList.sorted(new Comparator() { + public int compare(Expense e1, Expense e2) { + if (e1.getDate() == null || e2.getDate() == null) { + return 0; + } + return e1.getDate().compareTo(e2.getDate()); + } + }); + } + + public void sort(Comparator comparator) { + FXCollections.sort(internalList, comparator); + } + + public void reverse() { + FXCollections.reverse(internalList); + } + + public double getTotalSpendings() { + List costList = internalList.stream().map(e -> e.getItem().getCost().getAmount()) + .collect(Collectors.toList()); + return costList.stream().mapToDouble(f -> f.doubleValue()).sum(); + } +} diff --git a/src/main/java/seedu/address/model/epiggy/Goal.java b/src/main/java/seedu/address/model/epiggy/Goal.java new file mode 100644 index 000000000000..3d0676c63eed --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/Goal.java @@ -0,0 +1,46 @@ +package seedu.address.model.epiggy; + +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Name; + +//@@author kev-inc + +/** + * Represents a Goal in the expense book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Goal { + private final Name name; + private final Cost amount; + + public Goal(Name name, Cost amount) { + this.name = name; + this.amount = amount; + } + + public Name getName() { + return name; + } + + public Cost getAmount() { + return amount; + } + + @Override + public String toString() { + return String.format("%s - $%2s", name, amount); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Goal)) { + return false; + } + Goal otherGoal = (Goal) other; + return otherGoal.getName().equals(getName()) + && otherGoal.getAmount().equals(getAmount()); + } +} diff --git a/src/main/java/seedu/address/model/epiggy/SampleEPiggyDataUtil.java b/src/main/java/seedu/address/model/epiggy/SampleEPiggyDataUtil.java new file mode 100644 index 000000000000..5b1249bbddea --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/SampleEPiggyDataUtil.java @@ -0,0 +1,85 @@ +package seedu.address.model.epiggy; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import seedu.address.model.EPiggy; +import seedu.address.model.ReadOnlyEPiggy; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Item; +import seedu.address.model.epiggy.item.Name; +import seedu.address.model.epiggy.item.Period; +import seedu.address.model.tag.Tag; + +//@@author kev-inc + +/** + * Contains utility methods for populating {@code EPiggy} with sample data. + */ +public class SampleEPiggyDataUtil { + public static Expense[] getSampleExpenses() throws ParseException { + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + return new Expense[] { + new Allowance(new Item( + new Name("Allowance"), + new Cost(20), getTagSet("Allowance")), + sdf.parse("31/01/2019") + ), + new Expense(new Item( + new Name("Fishball Noodles"), + new Cost(4), + getTagSet("Lunch")), + sdf.parse("02/02/2019") + ) + }; + } + + public static Goal getSampleGoal() { + return new Goal(new Name("Nintendo Switch"), new Cost(499)); + } + + public static Budget[] getSampleBudget() throws ParseException { + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + return new Budget[] { + new Budget(new Cost(200), new Period(30), sdf.parse("01/04/2019")), + new Budget(new Cost(200), new Period(31), sdf.parse("01/03/2019")) + }; + } + + /** + * Returns a sample ePiggy with expenses, allowances, goal and budgets. + */ + public static ReadOnlyEPiggy getSampleEPiggy() { + EPiggy sampleEp = new EPiggy(); + try { + for (Expense sampleExpense : getSampleExpenses()) { + if (sampleExpense instanceof Allowance) { + sampleEp.addAllowance((Allowance) sampleExpense); + } else { + sampleEp.addExpense(sampleExpense); + } + } + Budget[] budgets = getSampleBudget(); + for (int i = 0; i < budgets.length; i++) { + sampleEp.addBudget(i, budgets[i]); + } + sampleEp.setGoal(getSampleGoal()); + } catch (ParseException e) { + e.printStackTrace(); + } + + return sampleEp; + } + + /** + * 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/model/epiggy/UniqueBudgetList.java b/src/main/java/seedu/address/model/epiggy/UniqueBudgetList.java new file mode 100644 index 000000000000..ddfca4e1c509 --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/UniqueBudgetList.java @@ -0,0 +1,174 @@ +package seedu.address.model.epiggy; + +import static java.util.Objects.requireNonNull; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.model.epiggy.Budget.CURRENT_BUDGET; + +import java.util.Date; +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import seedu.address.model.epiggy.exceptions.DuplicateBudgetException; + +//@@author tehwenyi + +/** + * A list of budgetList that enforces uniqueness between its elements and does not allow nulls. + * A budget is considered unique by comparing using {@code Budget#equals(Budget)}. As such, adding and updating of + * budgetList uses Budget#equals(Budget) for equality so as to ensure that the budget being added or updated is + * unique in terms of identity in the UniqueBudgetList. However, the removal of a budget uses Budget#equals(Object) so + * as to ensure that the budget with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + */ +public class UniqueBudgetList implements Iterable { + public static final int MAXIMUM_SIZE = 20; + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Adds a new budget to the specified index. + * @param index the index which the budget will be added at. + * @param toAdd the budget to be added. + */ + public void addAtIndex(int index, Budget toAdd) { + requireNonNull(toAdd); + internalList.add(index, toAdd); + limitSize(); + } + + /** + * Replaces the budget at index {@code index} with budget {@code toSet}. + * @param index the index which the budget will be added at. + * @param toSet the budget to be added. + */ + public void replaceAtIndex(int index, Budget toSet) { + requireNonNull(toSet); + internalList.set(index, toSet); + limitSize(); + } + + /** + * Gets the budget on the internal list with the corresponding index. + * There must be at least one budget in {@code internalList} + * @return the corresponding budget in {@code internalList}. + */ + public Budget getBudgetAtIndex(int index) { + return internalList.get(index); + } + + /** + * Gets the index of the budget based on the date. + * @return the index of the budget or -1 if the expense date is not in any of the budgets. + */ + public int getBudgetIndexBasedOnDate(Date date) { + requireAllNonNull(internalList, date); + + for (int i = 0; i < internalList.size(); i++) { + Budget toCheck = internalList.get(i); + if ((!toCheck.getStartDate().after(date)) && (!toCheck.getEndDate().before(date))) { + return i; + } + } + return -1; + } + + /** + * Gets the current budget's index. + * @return -1 if there is no current budget. + */ + public int getCurrentBudgetIndex() { + int index = 0; + while (index < internalList.size()) { + if (internalList.get(index).getStatus().equals(CURRENT_BUDGET)) { + return index; + } + index++; + } + return -1; + } + + /** + * Gets the size of internal list. + * @return the size of {@code internalList}. + */ + public int getBudgetListSize() { + return internalList.size(); + } + + /** + * Replaces the contents of this list with {@code budgetList}. + * {@code budgetList} must not contain duplicate budgetList. + * @param newBudgetList to replace. + */ + public void addBudgetList(List newBudgetList) { + requireAllNonNull(newBudgetList); + if (!budgetsAreUnique(newBudgetList)) { + throw new DuplicateBudgetException(); + } + + this.internalList.setAll(newBudgetList); + limitSize(); + } + + /** + * Returns true if {@code budgetList} contains only unique budgetList. + */ + private boolean budgetsAreUnique(List budgetList) { + for (int i = 0; i < budgetList.size() - 1; i++) { + for (int j = i + 1; j < budgetList.size(); j++) { + if (budgetList.get(i).equals(budgetList.get(j))) { + return false; + } + } + } + return true; + } + + /** + * Ensures the size of budgetList does not exceed {@code MAXIMUM_SIZE}. + * Deletes all budgets after the {@code MAXIMUM_SIZE} has exceeded. + */ + private void limitSize() { + requireNonNull(internalList); + int budgetListSize = internalList.size(); + if (budgetListSize > MAXIMUM_SIZE) { + for (int i = budgetListSize; i > MAXIMUM_SIZE; i--) { + internalList.remove(MAXIMUM_SIZE); + } + } + } + + /** + * Removes the budget with the specific index from the list. + * The budget of the index must exist in the list. + * @param index of the budget to be removed. + */ + public void remove(int index) { + internalList.remove(index, index + 1); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueBudgetList // instanceof handles nulls + && internalList.equals(((UniqueBudgetList) other).internalList)); + } +} diff --git a/src/main/java/seedu/address/model/epiggy/comparators/CompareExpenseByCost.java b/src/main/java/seedu/address/model/epiggy/comparators/CompareExpenseByCost.java new file mode 100644 index 000000000000..e1ca2ea18055 --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/comparators/CompareExpenseByCost.java @@ -0,0 +1,24 @@ +package seedu.address.model.epiggy.comparators; + +import java.util.Comparator; + +import seedu.address.model.epiggy.Expense; + +/** + * Comparator function for sorting Expenses by cost in descending order. + */ +public class CompareExpenseByCost implements Comparator { + + @Override + public boolean equals(Object obj) { + return obj instanceof CompareExpenseByCost; + } + + @Override + public int compare(Expense o1, Expense o2) { + if (o1.getItem().getCost() == null || o2.getItem().getCost() == null) { + return 0; + } + return o1.getItem().getCost().getAmount() < o2.getItem().getCost().getAmount() ? 1 : -1; + } +} diff --git a/src/main/java/seedu/address/model/epiggy/comparators/CompareExpenseByDate.java b/src/main/java/seedu/address/model/epiggy/comparators/CompareExpenseByDate.java new file mode 100644 index 000000000000..7cd4121ae94a --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/comparators/CompareExpenseByDate.java @@ -0,0 +1,24 @@ +package seedu.address.model.epiggy.comparators; + +import java.util.Comparator; + +import seedu.address.model.epiggy.Expense; + +/** + * Comparator function for sorting Expenses by date in with the latest date being first. + */ +public class CompareExpenseByDate implements Comparator { + + @Override + public boolean equals(Object obj) { + return obj instanceof CompareExpenseByDate; + } + + @Override + public int compare(Expense o1, Expense o2) { + if (o1.getDate() == null || o2.getDate() == null) { + return 0; + } + return o2.getDate().compareTo(o1.getDate()); + } +} diff --git a/src/main/java/seedu/address/model/epiggy/comparators/CompareExpenseByName.java b/src/main/java/seedu/address/model/epiggy/comparators/CompareExpenseByName.java new file mode 100644 index 000000000000..6456a2ce4b1f --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/comparators/CompareExpenseByName.java @@ -0,0 +1,23 @@ +package seedu.address.model.epiggy.comparators; + +import java.util.Comparator; + +import seedu.address.model.epiggy.Expense; + +/** + * Comparator function for sorting Expenses by name in lexicographical order. + */ +public class CompareExpenseByName implements Comparator { + @Override + public int compare(Expense o1, Expense o2) { + if (o1.getItem().getName() == null || o2.getItem().getName() == null) { + return 0; + } + return o1.getItem().getName().name.compareToIgnoreCase(o2.getItem().getName().name); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof CompareExpenseByName; + } +} diff --git a/src/main/java/seedu/address/model/epiggy/exceptions/DuplicateBudgetException.java b/src/main/java/seedu/address/model/epiggy/exceptions/DuplicateBudgetException.java new file mode 100644 index 000000000000..447775e5a9b4 --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/exceptions/DuplicateBudgetException.java @@ -0,0 +1,13 @@ +package seedu.address.model.epiggy.exceptions; + +//@@author tehwenyi + +/** + * Signals that the operation will result in duplicate Budgets (Budgets are considered duplicates if they have the same + * dates). + */ +public class DuplicateBudgetException extends RuntimeException { + public DuplicateBudgetException() { + super("Operation would result in duplicate budgets"); + } +} diff --git a/src/main/java/seedu/address/model/epiggy/item/Cost.java b/src/main/java/seedu/address/model/epiggy/item/Cost.java new file mode 100644 index 000000000000..a98f49159ad9 --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/item/Cost.java @@ -0,0 +1,69 @@ +package seedu.address.model.epiggy.item; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Represents a Cost. + * Guarantees: immutable + */ +public class Cost { + public static final String MESSAGE_CONSTRAINTS = "Cost should be an numerical amount not more than 6 digits."; + private static final Pattern AMOUNT_FORMAT = Pattern.compile("^(?!\\.?$)\\d{0,6}(\\.\\d{0,2})?$"); + private final double amount; + + public Cost(double amount) { + this.amount = amount; + } + + public Cost(String amount) { + requireNonNull(amount); + checkArgument(isValidCost(amount), MESSAGE_CONSTRAINTS); + this.amount = Double.parseDouble(amount); + } + + public Cost deduct(Cost amountToDeduct) { + return new Cost(this.amount - amountToDeduct.getAmount()); + } + + public double getAmount() { + return amount; + } + + /** + * Returns true if a given string is a valid Cost. + */ + public static boolean isValidCost(String test) { + try { + Matcher matcher = AMOUNT_FORMAT.matcher(test); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_CONSTRAINTS)); + } + double d = Double.parseDouble(test); + return d > 0. && d < 1000000.0; + } catch (NumberFormatException | ParseException e) { + return false; + } + } + + @Override + public String toString() { + return String.format("%.2f", amount); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Cost)) { + return false; + } + return amount == ((Cost) other).getAmount(); + } +} diff --git a/src/main/java/seedu/address/model/epiggy/item/Item.java b/src/main/java/seedu/address/model/epiggy/item/Item.java new file mode 100644 index 000000000000..5dee26a85b17 --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/item/Item.java @@ -0,0 +1,58 @@ +package seedu.address.model.epiggy.item; + +import java.util.Set; + +import seedu.address.model.tag.Tag; + +/** + * Represents an Item in the expense book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Item { + private final Name name; + private final Cost cost; + private final Set tags; + + public Item(Name name, Cost cost, Set tags) { + this.name = name; + this.cost = cost; + this.tags = tags; + } + + public Name getName() { + return name; + } + + public Cost getCost() { + return cost; + } + + public Set getTags() { + return tags; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(name) + .append(": $") + .append(cost) + .append("\nTags: ") + .append(tags); + return builder.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Item)) { + return false; + } + Item otherItem = (Item) other; + return otherItem.getName().equals(getName()) + && otherItem.getCost().equals(getCost()) + && otherItem.getTags().equals(getTags()); + } +} diff --git a/src/main/java/seedu/address/model/epiggy/item/Name.java b/src/main/java/seedu/address/model/epiggy/item/Name.java new file mode 100644 index 000000000000..f1c8a11085b0 --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/item/Name.java @@ -0,0 +1,40 @@ +package seedu.address.model.epiggy.item; + +import static java.util.Objects.requireNonNull; + +/** + * Represents an Item's name in the expense book. + * Guarantees: immutable} + */ +public class Name { + + public static final String MESSAGE_CONSTRAINTS = + "Name should have at least 1 alphanumeric character and less than 50 characters in length."; + public static final String VALIDATION_REGEX = "^[a-zA-Z0-9 ]{1,50}$"; + public final String name; + + public Name(String name) { + requireNonNull(name); + this.name = name; + } + + public static boolean isValidName(String name) { + return name.trim().matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Name)) { + return false; + } + return name.equals(((Name) other).name); + } +} diff --git a/src/main/java/seedu/address/model/epiggy/item/Period.java b/src/main/java/seedu/address/model/epiggy/item/Period.java new file mode 100644 index 000000000000..6dd922ad76ee --- /dev/null +++ b/src/main/java/seedu/address/model/epiggy/item/Period.java @@ -0,0 +1,73 @@ +package seedu.address.model.epiggy.item; + +import static java.util.Objects.requireNonNull; + +import static seedu.address.commons.util.AppUtil.checkArgument; + +//@@author tehwenyi + +/** + * Represents a time period. + * Guarantees: immutable + */ +public class Period { + public static final String MESSAGE_CONSTRAINTS = + "Time period is in terms of days and should only contain whole numbers of at least value 1.\n" + + "Time period cannot exceed 1 million days (1,000,000)"; + public static final String VALIDATION_REGEX = "\\b([1-9]|[1-8][0-9]|9[0-9]|[1-8][0-9]{2}|9[0-8][0-9]|99[0-9]" + + "|[1-8][0-9]{3}|9[0-8][0-9]{2}|99[0-8][0-9]|999[0-9]|[1-8][0-9]{4}|9[0-8][0-9]{3}|99[0-8][0-9]{2}" + + "|999[0-8][0-9]|9999[0-9]|[1-8][0-9]{5}|9[0-8][0-9]{4}|99[0-8][0-9]{3}|999[0-8][0-9]{2}|9999[0-8][0-9]" + + "|99999[0-9]|1000000)\\b"; + private final int timePeriod; + + public Period(int timePeriod) { + checkArgument(isValidPeriod(timePeriod), MESSAGE_CONSTRAINTS); + this.timePeriod = timePeriod; + } + + public Period(String period) { + requireNonNull(period); + checkArgument(isValidPeriod(period), MESSAGE_CONSTRAINTS); + this.timePeriod = Integer.parseInt(period); + } + + public int getTimePeriod() { + return timePeriod; + } + + /** + * Returns true if a given string is a valid time period. + */ + public static boolean isValidPeriod(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Returns true if a given string is a valid time period. + */ + public static boolean isValidPeriod(int test) { + if (test < 0 || test > 2147483647) { + return false; + } + return true; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof Period)) { + return false; + } + + Period p = (Period) o; + return this.timePeriod == p.getTimePeriod(); + } + + @Override + public String toString() { + return Integer.toString(timePeriod); + } +} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facfa..4a7313a942f4 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -4,8 +4,9 @@ import java.util.Set; import java.util.stream.Collectors; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.EPiggy; + +import seedu.address.model.ReadOnlyEPiggy; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -14,7 +15,7 @@ import seedu.address.model.tag.Tag; /** - * Contains utility methods for populating {@code AddressBook} with sample data. + * Contains utility methods for populating {@code EPiggy} with sample data. */ public class SampleDataUtil { public static Person[] getSamplePersons() { @@ -40,8 +41,8 @@ public static Person[] getSamplePersons() { }; } - public static ReadOnlyAddressBook getSampleAddressBook() { - AddressBook sampleAb = new AddressBook(); + public static ReadOnlyEPiggy getSampleEPiggy() { + EPiggy sampleAb = new EPiggy(); for (Person samplePerson : getSamplePersons()) { sampleAb.addPerson(samplePerson); } diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/seedu/address/storage/AddressBookStorage.java deleted file mode 100644 index 4599182b3f92..000000000000 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ /dev/null @@ -1,45 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * Represents a storage for {@link seedu.address.model.AddressBook}. - */ -public interface AddressBookStorage { - - /** - * Returns the file path of the data file. - */ - Path getAddressBookFilePath(); - - /** - * Returns AddressBook data as a {@link ReadOnlyAddressBook}. - * Returns {@code Optional.empty()} if storage file is not found. - * @throws DataConversionException if the data in storage is not in the expected format. - * @throws IOException if there was any problem when reading from the storage. - */ - Optional readAddressBook() throws DataConversionException, IOException; - - /** - * @see #getAddressBookFilePath() - */ - Optional readAddressBook(Path filePath) throws DataConversionException, IOException; - - /** - * Saves the given {@link ReadOnlyAddressBook} to the storage. - * @param addressBook cannot be null. - * @throws IOException if there was any problem writing to the file. - */ - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - - /** - * @see #saveAddressBook(ReadOnlyAddressBook) - */ - void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/EPiggyStorage.java b/src/main/java/seedu/address/storage/EPiggyStorage.java new file mode 100644 index 000000000000..f868b77e4af3 --- /dev/null +++ b/src/main/java/seedu/address/storage/EPiggyStorage.java @@ -0,0 +1,53 @@ +package seedu.address.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.model.EPiggy; +import seedu.address.model.ReadOnlyEPiggy; + +/** + * Represents a storage for {@link EPiggy}. + */ +public interface EPiggyStorage { + + /** + * Returns the file path of the data file. + */ + Path getEPiggyFilePath(); + + /** + * Returns EPiggy data as a {@link ReadOnlyEPiggy}. + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readEPiggy() throws DataConversionException, IOException; + + /** + * @see #getEPiggyFilePath() + */ + Optional readEPiggy(Path filePath) throws DataConversionException, IOException; + + /** + * Saves the given {@link ReadOnlyEPiggy} to the storage. + * @param ePiggy cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveEPiggy(ReadOnlyEPiggy ePiggy) throws IOException; + + /** + * @see #saveEPiggy(ReadOnlyEPiggy) + */ + void saveEPiggy(ReadOnlyEPiggy ePiggy, Path filePath) throws IOException; + + /** + * Creates a backup file for {@link ReadOnlyEPiggy} + * @param ePiggy + * @throws IOException + */ + void backupEPiggy(ReadOnlyEPiggy ePiggy) throws IOException; + +} diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java deleted file mode 100644 index dfab9daaa0d3..000000000000 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ /dev/null @@ -1,80 +0,0 @@ -package seedu.address.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 seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.commons.util.FileUtil; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * A class to access AddressBook data stored as a json file on the hard disk. - */ -public class JsonAddressBookStorage implements AddressBookStorage { - - private static final Logger logger = LogsCenter.getLogger(JsonAddressBookStorage.class); - - private Path filePath; - - public JsonAddressBookStorage(Path filePath) { - this.filePath = filePath; - } - - public Path getAddressBookFilePath() { - return filePath; - } - - @Override - public Optional readAddressBook() throws DataConversionException { - return readAddressBook(filePath); - } - - /** - * Similar to {@link #readAddressBook()}. - * - * @param filePath location of the data. Cannot be null. - * @throws DataConversionException if the file is not in the correct format. - */ - public Optional readAddressBook(Path filePath) throws DataConversionException { - requireNonNull(filePath); - - Optional jsonAddressBook = JsonUtil.readJsonFile( - filePath, JsonSerializableAddressBook.class); - if (!jsonAddressBook.isPresent()) { - return Optional.empty(); - } - - try { - return Optional.of(jsonAddressBook.get().toModelType()); - } catch (IllegalValueException ive) { - logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); - throw new DataConversionException(ive); - } - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, filePath); - } - - /** - * Similar to {@link #saveAddressBook(ReadOnlyAddressBook)}. - * - * @param filePath location of the data. Cannot be null. - */ - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - requireNonNull(addressBook); - requireNonNull(filePath); - - FileUtil.createIfMissing(filePath); - JsonUtil.saveJsonFile(new JsonSerializableAddressBook(addressBook), filePath); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonEPiggyStorage.java b/src/main/java/seedu/address/storage/JsonEPiggyStorage.java new file mode 100644 index 000000000000..ee0b6310ed90 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonEPiggyStorage.java @@ -0,0 +1,82 @@ +package seedu.address.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.FileUtil; +import seedu.address.commons.util.JsonUtil; + +import seedu.address.model.ReadOnlyEPiggy; +import seedu.address.storage.epiggy.EPiggyStorage; +import seedu.address.storage.epiggy.JsonSerializableEPiggy; + +/** + * A class to access EPiggy data stored as a json file on the hard disk. + */ +public class JsonEPiggyStorage implements EPiggyStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonEPiggyStorage.class); + + private Path filePath; + private Path backupFilePath; + + public JsonEPiggyStorage(Path filePath) { + this.filePath = filePath; + backupFilePath = Paths.get(filePath.toString() + ".backup"); + } + + @Override + public Path getEPiggyFilePath() { + return filePath; + } + + @Override + public Optional readEPiggy() throws DataConversionException, IOException { + return readEPiggy(filePath); + } + + @Override + public Optional readEPiggy(Path filePath) throws DataConversionException, IOException { + requireNonNull(filePath); + + Optional jsonEPiggy = JsonUtil.readJsonFile( + filePath, JsonSerializableEPiggy.class); + if (!jsonEPiggy.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonEPiggy.get().toModelType()); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveEPiggy(ReadOnlyEPiggy ePiggy) throws IOException { + saveEPiggy(ePiggy, filePath); + } + + @Override + public void saveEPiggy(ReadOnlyEPiggy ePiggy, Path filePath) throws IOException { + requireNonNull(ePiggy); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableEPiggy(ePiggy), filePath); + } + + @Override + public void backupEPiggy(ReadOnlyEPiggy ePiggy) throws IOException { + saveEPiggy(ePiggy, backupFilePath); + } +} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d4..c3937ef3a37c 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -9,52 +9,52 @@ 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.EPiggy; +import seedu.address.model.ReadOnlyEPiggy; import seedu.address.model.person.Person; /** - * An Immutable AddressBook that is serializable to JSON format. + * An Immutable EPiggy that is serializable to JSON format. */ @JsonRootName(value = "addressbook") -class JsonSerializableAddressBook { +class JsonSerializableEPiggy { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; private final List persons = new ArrayList<>(); /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableEPiggy} with the given persons. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { + public JsonSerializableEPiggy(@JsonProperty("persons") List persons) { this.persons.addAll(persons); } /** - * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. + * Converts a given {@code ReadOnlyEPiggy} into this class for Jackson use. * - * @param source future changes to this will not affect the created {@code JsonSerializableAddressBook}. + * @param source future changes to this will not affect the created {@code JsonSerializableEPiggy}. */ - public JsonSerializableAddressBook(ReadOnlyAddressBook source) { + public JsonSerializableEPiggy(ReadOnlyEPiggy source) { persons.addAll(source.getPersonList().stream().map(JsonAdaptedPerson::new).collect(Collectors.toList())); } /** - * Converts this address book into the model's {@code AddressBook} object. + * Converts this address book into the model's {@code EPiggy} object. * * @throws IllegalValueException if there were any data constraints violated. */ - public AddressBook toModelType() throws IllegalValueException { - AddressBook addressBook = new AddressBook(); + public EPiggy toModelType() throws IllegalValueException { + EPiggy ePiggy = new EPiggy(); for (JsonAdaptedPerson jsonAdaptedPerson : persons) { Person person = jsonAdaptedPerson.toModelType(); - if (addressBook.hasPerson(person)) { + if (ePiggy.hasPerson(person)) { throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); } - addressBook.addPerson(person); + ePiggy.addPerson(person); } - return addressBook; + return ePiggy; } } diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index beda8bd9f11b..a9c4a3e50419 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -5,14 +5,16 @@ import java.util.Optional; import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyEPiggy; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; +import seedu.address.storage.epiggy.EPiggyStorage; + /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends EPiggyStorage, UserPrefsStorage { @Override Optional readUserPrefs() throws DataConversionException, IOException; @@ -21,12 +23,13 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; @Override - Path getAddressBookFilePath(); + Path getEPiggyFilePath(); @Override - Optional readAddressBook() throws DataConversionException, IOException; + Optional readEPiggy() throws DataConversionException, IOException; @Override - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + void saveEPiggy(ReadOnlyEPiggy ePiggy) throws IOException; + } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index e4f452b6cbf4..dff3252d17e8 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -7,23 +7,25 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyEPiggy; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; +import seedu.address.storage.epiggy.EPiggyStorage; + /** - * Manages storage of AddressBook data in local storage. + * Manages storage of EPiggy data in local storage. */ public class StorageManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); - private AddressBookStorage addressBookStorage; + private EPiggyStorage ePiggyStorage; private UserPrefsStorage userPrefsStorage; - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { + public StorageManager(EPiggyStorage ePiggyStorage, UserPrefsStorage userPrefsStorage) { super(); - this.addressBookStorage = addressBookStorage; + this.ePiggyStorage = ePiggyStorage; this.userPrefsStorage = userPrefsStorage; } @@ -45,33 +47,39 @@ public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { } - // ================ AddressBook methods ============================== + // ================ EPiggy methods ============================== @Override - public Path getAddressBookFilePath() { - return addressBookStorage.getAddressBookFilePath(); + public Path getEPiggyFilePath() { + return ePiggyStorage.getEPiggyFilePath(); } @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(addressBookStorage.getAddressBookFilePath()); + public Optional readEPiggy() throws DataConversionException, IOException { + return readEPiggy(ePiggyStorage.getEPiggyFilePath()); } @Override - public Optional readAddressBook(Path filePath) throws DataConversionException, IOException { + public Optional readEPiggy(Path filePath) throws DataConversionException, IOException { logger.fine("Attempting to read data from file: " + filePath); - return addressBookStorage.readAddressBook(filePath); + return ePiggyStorage.readEPiggy(filePath); } @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath()); + public void saveEPiggy(ReadOnlyEPiggy ePiggy) throws IOException { + saveEPiggy(ePiggy, ePiggyStorage.getEPiggyFilePath()); } @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { + public void saveEPiggy(ReadOnlyEPiggy ePiggy, Path filePath) throws IOException { logger.fine("Attempting to write to data file: " + filePath); - addressBookStorage.saveAddressBook(addressBook, filePath); + ePiggyStorage.saveEPiggy(ePiggy, filePath); + } + + @Override + public void backupEPiggy(ReadOnlyEPiggy ePiggy) throws IOException { + logger.fine("Creating a backup file: " + getEPiggyFilePath() + ".backup"); + ePiggyStorage.backupEPiggy(ePiggy); } } diff --git a/src/main/java/seedu/address/storage/epiggy/EPiggyStorage.java b/src/main/java/seedu/address/storage/epiggy/EPiggyStorage.java new file mode 100644 index 000000000000..0250907423e8 --- /dev/null +++ b/src/main/java/seedu/address/storage/epiggy/EPiggyStorage.java @@ -0,0 +1,28 @@ +package seedu.address.storage.epiggy; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.model.EPiggy; +import seedu.address.model.ReadOnlyEPiggy; + +//@@author kev-inc + +/** + * Represents a storage for {@link EPiggy}. + */ +public interface EPiggyStorage { + Path getEPiggyFilePath(); + + Optional readEPiggy() throws DataConversionException, IOException; + + Optional readEPiggy(Path filePath) throws DataConversionException, IOException; + + void saveEPiggy(ReadOnlyEPiggy ePiggy) throws IOException; + + void saveEPiggy(ReadOnlyEPiggy ePiggy, Path filePath) throws IOException; + + void backupEPiggy(ReadOnlyEPiggy ePiggy) throws IOException; +} diff --git a/src/main/java/seedu/address/storage/epiggy/JsonAdaptedBudget.java b/src/main/java/seedu/address/storage/epiggy/JsonAdaptedBudget.java new file mode 100644 index 000000000000..6cd8c746961f --- /dev/null +++ b/src/main/java/seedu/address/storage/epiggy/JsonAdaptedBudget.java @@ -0,0 +1,93 @@ +package seedu.address.storage.epiggy; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.model.epiggy.Budget; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Period; + +//@@author kev-inc + +/** + * Json friendly version of (@Link Budget) + */ +public class JsonAdaptedBudget { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Budget's %s field is missing!"; + + private final String amount; + private final String startDate; + private final String period; + private final String remainingAmount; + + /** + * Constructs a {@code JsonAdaptedBudget} with the given expense details. + */ + @JsonCreator + public JsonAdaptedBudget(@JsonProperty("amount") String amount, + @JsonProperty("startDate") String startDate, + @JsonProperty("period") String period, + @JsonProperty("remainingAmount") String remainingAmount) { + this.amount = amount; + this.startDate = startDate; + this.period = period; + this.remainingAmount = remainingAmount; + + } + /** + * Converts a given {@code Budget} into this class for Jackson use. + */ + public JsonAdaptedBudget(Budget source) { + amount = String.valueOf(source.getBudgetedAmount().getAmount()); + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + Date sd = source.getStartDate(); + startDate = sdf.format(sd); + period = source.getPeriod().toString(); + remainingAmount = String.valueOf(source.getRemainingAmount().getAmount()); + } + + /** + * Converts this Jackson-friendly adapted budget object into the model's {@code Budget} object. + */ + public Budget toModelType() throws IllegalValueException { + if (amount == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Cost.class.getSimpleName())); + } + if (!Cost.isValidCost(amount)) { + throw new IllegalValueException(String.format(Cost.MESSAGE_CONSTRAINTS)); + } + final Cost modelAmount = new Cost(amount); + + if (startDate == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Date.class.getSimpleName())); + } + + final Date modelStartDate = ParserUtil.parseDate(startDate); + + if (period == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Period.class.getSimpleName())); + } + if (!Period.isValidPeriod(period)) { + throw new IllegalValueException(Period.MESSAGE_CONSTRAINTS); + } + final Period modelPeriod = new Period(period); + + if (remainingAmount == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Cost.class.getSimpleName())); + } + if (!Cost.isValidCost(remainingAmount)) { + throw new IllegalValueException(String.format(Cost.MESSAGE_CONSTRAINTS)); + } + final Cost modelRemaining = new Cost(Double.parseDouble(remainingAmount)); + + Budget b = new Budget(modelAmount, modelPeriod, modelStartDate); + b.setRemainingAmount(modelRemaining); + return b; + } +} diff --git a/src/main/java/seedu/address/storage/epiggy/JsonAdaptedExpense.java b/src/main/java/seedu/address/storage/epiggy/JsonAdaptedExpense.java new file mode 100644 index 000000000000..2ca704adbf57 --- /dev/null +++ b/src/main/java/seedu/address/storage/epiggy/JsonAdaptedExpense.java @@ -0,0 +1,108 @@ +package seedu.address.storage.epiggy; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Item; +import seedu.address.model.epiggy.item.Name; +import seedu.address.model.tag.Tag; + +//@@author kev-inc + +/** + * Json friendly version of (@Link Expense) + */ +public class JsonAdaptedExpense { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Expense's %s field is missing!"; + + private final String name; + private final String cost; + private final String date; + private final String type; + private final List tags = new ArrayList<>(); + + /** + * Constructs a {@code JsonAdaptedExpense} with the given expense details. + */ + @JsonCreator + public JsonAdaptedExpense(@JsonProperty("name") String name, + @JsonProperty("cost") String cost, + @JsonProperty("date") String date, + @JsonProperty("type") String type, + @JsonProperty("tags") List tags) { + this.name = name; + this.cost = cost; + this.date = date; + this.type = type; + if (tags != null) { + this.tags.addAll(tags); + } + } + + /** + * Converts a given {@code Expense} into this class for Jackson use. + */ + public JsonAdaptedExpense(Expense source) { + name = source.getItem().getName().name; + cost = String.valueOf(source.getItem().getCost().getAmount()); + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + date = sdf.format(source.getDate()); + if (source instanceof Allowance) { + type = "allowance"; + } else { + type = "expense"; + } + tags.addAll(source.getItem().getTags().stream() + .map(JsonAdaptedTags::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted person object into the model's {@code Expense} object. + */ + public Expense toModelType() throws IllegalValueException { + final List expenseTags = new ArrayList<>(); + for (JsonAdaptedTags tag: tags) { + expenseTags.add(tag.toModelType()); + } + + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); + } + final Name modelName = new Name(name); + + if (cost == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Cost.class.getSimpleName())); + } + if (!Cost.isValidCost(cost)) { + throw new IllegalValueException(Cost.MESSAGE_CONSTRAINTS); + } + final Cost modelCost = new Cost(cost); + + if (date == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Date.class.getSimpleName())); + } + final Date modelDate = ParserUtil.parseDate(date); + + final Set modelTags = new HashSet<>(expenseTags); + + if (type.equals("allowance")) { + return new Allowance(new Item(modelName, modelCost, modelTags), modelDate); + } + return new Expense(new Item(modelName, modelCost, modelTags), modelDate); + } +} diff --git a/src/main/java/seedu/address/storage/epiggy/JsonAdaptedGoal.java b/src/main/java/seedu/address/storage/epiggy/JsonAdaptedGoal.java new file mode 100644 index 000000000000..7e1e3da18369 --- /dev/null +++ b/src/main/java/seedu/address/storage/epiggy/JsonAdaptedGoal.java @@ -0,0 +1,80 @@ +package seedu.address.storage.epiggy; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.epiggy.Goal; +import seedu.address.model.epiggy.item.Cost; +import seedu.address.model.epiggy.item.Name; + +//@@author kev-inc + +/** + * Json friendly version of (@Link Goal) + */ +public class JsonAdaptedGoal { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Goal's %s field is missing!"; + + private final Boolean isNull; + private final String name; + private final String cost; + + /** + * Constructs a {@code JsonAdaptedGoal} with the given goal details. + */ + @JsonCreator + public JsonAdaptedGoal( + @JsonProperty("isNull") Boolean isNull, + @JsonProperty("name") String name, + @JsonProperty("cost") String cost) { + this.isNull = isNull; + this.name = name; + this.cost = cost; + } + + /** + * Converts a given {@code Goal} into this class for Jackson use. + */ + public JsonAdaptedGoal(Goal source) { + if (source == null) { + isNull = true; + name = null; + cost = null; + } else { + isNull = false; + name = source.getName().toString(); + cost = String.valueOf(source.getAmount().getAmount()); + } + } + + /** + * Converts this Jackson-friendly adapted goal object into the model's {@code Goal} object. + * + * @return + * @throws IllegalValueException + */ + public Goal toModelType() throws IllegalValueException { + if (isNull) { + return null; + } + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); + } + if (!Name.isValidName(name)) { + throw new IllegalValueException(String.format(Name.MESSAGE_CONSTRAINTS)); + } + final Name modelName = new Name(name); + + if (cost == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Cost.class.getSimpleName())); + } + if (!Cost.isValidCost(cost)) { + throw new IllegalValueException(Cost.MESSAGE_CONSTRAINTS); + } + final Cost modelCost = new Cost(cost); + return new Goal(modelName, modelCost); + } + +} diff --git a/src/main/java/seedu/address/storage/epiggy/JsonAdaptedTags.java b/src/main/java/seedu/address/storage/epiggy/JsonAdaptedTags.java new file mode 100644 index 000000000000..40c69c75cc64 --- /dev/null +++ b/src/main/java/seedu/address/storage/epiggy/JsonAdaptedTags.java @@ -0,0 +1,47 @@ +package seedu.address.storage.epiggy; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.tag.Tag; + +//@@author kev-inc + +/** + * Jackson-friendly version of {@link Tag}. + */ +class JsonAdaptedTags { + + private final String tagName; + + /** + * Constructs a {@code JsonAdaptedTags} with the given {@code tagName}. + */ + @JsonCreator + public JsonAdaptedTags(String tagName) { + this.tagName = tagName; + } + + /** + * Converts a given {@code Tag} into this class for Jackson use. + */ + public JsonAdaptedTags(Tag source) { + tagName = source.tagName; + } + + @JsonValue + public String getTagName() { + return tagName; + } + + /** + * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. + */ + public Tag toModelType() throws IllegalValueException { + if (!Tag.isValidTagName(tagName)) { + throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); + } + return new Tag(tagName); + } +} diff --git a/src/main/java/seedu/address/storage/epiggy/JsonEPiggyStorage.java b/src/main/java/seedu/address/storage/epiggy/JsonEPiggyStorage.java new file mode 100644 index 000000000000..52a0ae54a6ff --- /dev/null +++ b/src/main/java/seedu/address/storage/epiggy/JsonEPiggyStorage.java @@ -0,0 +1,79 @@ +package seedu.address.storage.epiggy; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.FileUtil; +import seedu.address.commons.util.JsonUtil; +import seedu.address.model.ReadOnlyEPiggy; + +//@@author kev-inc + +/** + * A class to access epiggy data stored as a json file on the hard disk. + */ +public class JsonEPiggyStorage implements EPiggyStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonEPiggyStorage.class); + + private Path filePath; + private Path backupFilePath; + + public JsonEPiggyStorage(Path filePath) { + this.filePath = filePath; + backupFilePath = Paths.get(filePath.toString() + ".backup"); + } + + @Override + public Path getEPiggyFilePath() { + return filePath; + } + + @Override + public Optional readEPiggy() throws DataConversionException { + return readEPiggy(filePath); + } + + @Override + public Optional readEPiggy(Path filePath) throws DataConversionException { + requireNonNull(filePath); + + Optional jsonEPiggy = JsonUtil.readJsonFile( + filePath, JsonSerializableEPiggy.class); + if (!jsonEPiggy.isPresent()) { + return Optional.empty(); + } + try { + return Optional.of(jsonEPiggy.get().toModelType()); + } catch (IllegalValueException e) { + logger.info("Illegal values found in " + filePath + ": " + e.getMessage()); + throw new DataConversionException(e); + } + } + + @Override + public void saveEPiggy(ReadOnlyEPiggy ePiggy) throws IOException { + saveEPiggy(ePiggy, filePath); + } + + @Override + public void saveEPiggy(ReadOnlyEPiggy ePiggy, Path filePath) throws IOException { + requireNonNull(ePiggy); + requireNonNull(filePath); + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableEPiggy(ePiggy), filePath); + } + + @Override + public void backupEPiggy(ReadOnlyEPiggy ePiggy) throws IOException { + saveEPiggy(ePiggy, backupFilePath); + } +} diff --git a/src/main/java/seedu/address/storage/epiggy/JsonSerializableEPiggy.java b/src/main/java/seedu/address/storage/epiggy/JsonSerializableEPiggy.java new file mode 100644 index 000000000000..3f36051a13f3 --- /dev/null +++ b/src/main/java/seedu/address/storage/epiggy/JsonSerializableEPiggy.java @@ -0,0 +1,75 @@ +package seedu.address.storage.epiggy; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.EPiggy; +import seedu.address.model.ReadOnlyEPiggy; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.Budget; +import seedu.address.model.epiggy.Expense; + +//@@author kev-inc + +/** + * An Immutable epiggy that is serializable to JSON format. + */ +@JsonRootName(value = "epiggy") +public class JsonSerializableEPiggy { + private final List expenses = new ArrayList<>(); + private final List budgets = new ArrayList<>(); + private final JsonAdaptedGoal goal; + + + /** + * Constructs a {@code JsonSerializableEPiggy} with the given expense. + */ + @JsonCreator + public JsonSerializableEPiggy(@JsonProperty("expenses") List expenses, + @JsonProperty("goal") JsonAdaptedGoal goal, + @JsonProperty("budgets") List budgets) { + this.expenses.addAll(expenses); + this.budgets.addAll(budgets); + this.goal = goal; + } + + /** + * Converts a given {@code ReadOnlyEPiggy} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableEPiggy}. + */ + public JsonSerializableEPiggy(ReadOnlyEPiggy source) { + expenses.addAll(source.getExpenseList().stream().map(JsonAdaptedExpense::new).collect(Collectors.toList())); + budgets.addAll(source.getBudgetList().stream().map(JsonAdaptedBudget::new).collect(Collectors.toList())); + goal = new JsonAdaptedGoal(source.getGoal().get()); + } + + /** + * Converts this epiggy into the model's {@code EPiggy} object. + * + * @throws IllegalValueException if there were any data constraints violated. + */ + public EPiggy toModelType() throws IllegalValueException { + EPiggy ePiggy = new EPiggy(); + for (JsonAdaptedExpense jsonAdaptedExpense : expenses) { + Expense expense = jsonAdaptedExpense.toModelType(); + if (expense instanceof Allowance) { + ePiggy.addAllowance((Allowance) expense); + } else { + ePiggy.addExpense(expense); + } + } + for (int i = 0; i < budgets.size(); i++) { + Budget budget = budgets.get(i).toModelType(); + ePiggy.addBudget(i, budget); + } + ePiggy.setGoal(goal.toModelType()); + return ePiggy; + } +} diff --git a/src/main/java/seedu/address/ui/BrowserPanel.java b/src/main/java/seedu/address/ui/BrowserPanel.java index 1e76124a59e2..53876e01c8d1 100644 --- a/src/main/java/seedu/address/ui/BrowserPanel.java +++ b/src/main/java/seedu/address/ui/BrowserPanel.java @@ -22,7 +22,7 @@ public class BrowserPanel extends UiPart { public static final URL DEFAULT_PAGE = requireNonNull(MainApp.class.getResource(FXML_FILE_FOLDER + "default.html")); - public static final String SEARCH_PAGE_URL = "https://se-edu.github.io/dummy-search-page/?name="; + public static final String SEARCH_PAGE_URL = "https://se-education.org/dummy-search-page/?name="; private static final String FXML = "BrowserPanel.fxml"; diff --git a/src/main/java/seedu/address/ui/BudgetCard.java b/src/main/java/seedu/address/ui/BudgetCard.java new file mode 100644 index 000000000000..082178fa16ac --- /dev/null +++ b/src/main/java/seedu/address/ui/BudgetCard.java @@ -0,0 +1,94 @@ +package seedu.address.ui; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.epiggy.Budget; + +//@@author tehwenyi + +/** + * A UI component that displays information of a {@code Budget}. + */ +public class BudgetCard extends UiPart { + private static final Logger logger = LogsCenter.getLogger(BudgetCard.class); + + private static final String FXML = "BudgetListCard.fxml"; + + public final Budget budget; + + @FXML + private Label title; + @FXML + private Label amount; + @FXML + private Label startDate; + @FXML + private Label endDate; + @FXML + private Label period; + @FXML + private Label status; + @FXML + private Label remainingAmount; + @FXML + private Label remainingDays; + @FXML + private Label notification; + + public BudgetCard(int displayedIndex, Budget budget) { + super(FXML); + this.budget = budget; + title.setText(displayedIndex + ". " + budget.getStatus() + " Budget"); + amount.setText("Amount: $" + budget.getBudgetedAmount().toString()); + + DateFormat dateFormat = new SimpleDateFormat("dd MMM yyyy (E)"); + startDate.setText("Start Date: " + dateFormat.format(budget.getStartDate())); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(budget.getEndDate()); + calendar.add(Calendar.DAY_OF_MONTH, -1); + endDate.setText("End Date: " + dateFormat.format(calendar.getTime())); + period.setText("Period of Budget: " + budget.getPeriod().toString() + " days"); + + status.setText("Status"); + if (budget.getRemainingAmount().getAmount() >= 0) { + remainingAmount.setText("Amount remaining: $" + budget.getRemainingAmount().toString()); + } else { + remainingAmount.setText("Amount exceeded: $" + budget.getPositiveRemainingAmount().toString()); + } + remainingDays.setText("Days remaining: " + budget.getRemainingDays().toString() + " days"); + + if (budget.getRemainingAmount().getAmount() < 0) { + notification.setText("You have exceeded your budget!"); + notification.setStyle("-fx-font-weight: bold; -fx-border-color: firebrick;" + + "-fx-text-fill: white; -fx-background-color: crimson;"); + } else if (budget.getRemainingAmount().getAmount() == 0) { + notification.setText("You have $0 left of your budget!"); + notification.setStyle("-fx-font-weight: bold; -fx-border-color: orchid; " + + "-fx-text-fill: white; -fx-background-color: mediumorchid;"); + } else if (budget.getRemainingAmount().getAmount() < (budget.getBudgetedAmount().getAmount() / 5)) { + notification.setText("You have spent more than 80% of your budget. \n" + + "Please control your expenses!"); + notification.setStyle("-fx-font-weight: bold; -fx-border-color: tomato; " + + "-fx-text-fill: white; -fx-background-color: coral;"); + } else { + notification.setText("“Save money and money will save you.”\n" + + "Remember to spend wisely!"); + notification.setStyle("-fx-font-weight: bold; -fx-border-color: thistle;" + + "-fx-text-fill: white; -fx-background-color: plum;"); + } + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + return (other == this); + } +} diff --git a/src/main/java/seedu/address/ui/BudgetPanel.java b/src/main/java/seedu/address/ui/BudgetPanel.java new file mode 100644 index 000000000000..7c20bff3a9cf --- /dev/null +++ b/src/main/java/seedu/address/ui/BudgetPanel.java @@ -0,0 +1,49 @@ +package seedu.address.ui; + +import java.util.function.Consumer; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.epiggy.Budget; + +//@@author tehwenyi + +/** + * Panel containing the budgets and their information. + */ +public class BudgetPanel extends UiPart { + private static final String FXML = "BudgetPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(BudgetPanel.class); + + @FXML + private ListView budgetView; + + public BudgetPanel(ObservableList budgetList, Consumer setCurrentBudget) { + super(FXML); + budgetView.setItems(budgetList); + budgetView.setCellFactory(listView -> new BudgetListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Budget} using a {@code BudgetCard}. + */ + class BudgetListViewCell extends ListCell { + @Override + protected void updateItem(Budget budget, boolean empty) { + super.updateItem(budget, empty); + + if (empty || budget == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new BudgetCard(getIndex() + 1, budget).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index bf09f3dcbea6..ef0965b6d418 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -1,9 +1,11 @@ package seedu.address.ui; +import java.util.ArrayList; import java.util.List; import javafx.collections.ObservableList; import javafx.fxml.FXML; +import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; @@ -18,14 +20,41 @@ public class CommandBox extends UiPart { public static final String ERROR_STYLE_CLASS = "error"; private static final String FXML = "CommandBox.fxml"; - + private static int tabCount = 0; // count for "tab" pressed private final CommandExecutor commandExecutor; private final List history; private ListElementPointer historySnapshot; + private String[] commandArr = { + "addAllowance n/ $/ t/ d/", + "addBudget $/ p/ d/", + "addExpense n/ $/ t/ d/", + "clear", + "deleteAllowance ", + "deleteBudget ", + "deleteExpense ", + "editAllowance n/ $/ d/ t/", + "editBudget $/ p/ d/", + "editExpense n/ $/ d/ t/", + "exit", + "find n/ t/ d/ $/", + "help", + "history", + "list", + "redo", + "report d/", + "reverseList", + "setGoal n/ $/", + "sort n/ d/ $/", + "undo", + }; + @FXML private TextField commandTextField; + @FXML + private TextArea resultDisplay; + public CommandBox(CommandExecutor commandExecutor, List history) { super(FXML); this.commandExecutor = commandExecutor; @@ -34,7 +63,34 @@ public CommandBox(CommandExecutor commandExecutor, List history) { commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); historySnapshot = new ListElementPointer(history); } + //@@author yunjun199321 + /** + * Find set of similar prefix keywords. + * + * @param stringTryToMatch User enters prefix + * @param commandInArray Command checklist + * @return Set of same prefix commands. + */ + private static String[] findString(String stringTryToMatch, String[] commandInArray) { + if (stringTryToMatch.length() == 0) { + // partOfString is null, return empty array + return new String[0]; + } + ArrayList resultArr = new ArrayList<>(); + // for each elements in strings, compare with part of the string. + for (int i = 0; i < commandInArray.length; i++) { + if (stringTryToMatch.length() > commandInArray[i].length()) { + // do nothing if the length of user input string longer than keyword. + continue; + } else if (stringTryToMatch.equalsIgnoreCase((commandInArray[i] + .substring(0, stringTryToMatch.trim().length())))) { + resultArr.add(commandInArray[i]); + } + } + return resultArr.toArray(new String[resultArr.size()]); + } + //@@author /** * Handles the key press event, {@code keyEvent}. */ @@ -52,11 +108,48 @@ private void handleKeyPress(KeyEvent keyEvent) { keyEvent.consume(); navigateToNextInput(); break; + + case TAB: + keyEvent.consume(); + autoCompleteText(); + break; default: // let JavaFx handle the keypress } } - + //@@author yunjun199321 + /** + * autocomplete text. + * Compare user input with the commandArr in the commands list. + * commandBox shows found command + */ + private void autoCompleteText() { + String[] inputString = commandTextField.getText().split(" "); //split with white space + String partOfString = inputString[inputString.length - 1]; // get the last element of input string + String[] results = findString(partOfString, commandArr); + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < inputString.length - 1; i++) { + /* group user inputs */ + stringBuilder.append(inputString[i].trim()); + stringBuilder.append(" "); + } + // updates user + found keyword + if (results.length == 0) { + /* if result is null, nothing match */ + stringBuilder.append(inputString[inputString.length - 1]); // adds back last element of input string + } else { + // append match keyword to user input + if (tabCount >= results.length) { + tabCount = 0; // reset tabCount when tabCount bigger than the number of match commandArr + } + stringBuilder.append(results[tabCount]); // adds found keyword + } + commandTextField.requestFocus(); // set focus back to the textfield + commandTextField.setText(stringBuilder.toString()); // updates the textfield + commandTextField.positionCaret(stringBuilder.length()); // set caret after the new text + tabCount++; + } + //@@author /** * Updates the text field with the previous input in {@code historySnapshot}, * if there exists a previous input in {@code historySnapshot} diff --git a/src/main/java/seedu/address/ui/ExpenseCard.java b/src/main/java/seedu/address/ui/ExpenseCard.java new file mode 100644 index 000000000000..15ae8230010f --- /dev/null +++ b/src/main/java/seedu/address/ui/ExpenseCard.java @@ -0,0 +1,108 @@ +package seedu.address.ui; + +import java.text.SimpleDateFormat; +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.Expense; + +/** + * An UI component that displays information of a {@code Person}. + */ +public class ExpenseCard extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(ExpenseCard.class); + + private static final String FXML = "ExpenseListCard.fxml"; + private static final String[] TAG_COLOR_STYLES = + { "turquoise", "orange", "yellow", "green", "black", "blue", "pink", "grey" }; + + /** + * 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 EPiggy level 4 + */ + + public final Expense expense; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label name; + @FXML + private Label cost; + @FXML + private Label date; + @FXML + private FlowPane typeTag; + @FXML + private FlowPane tags; + + public ExpenseCard(Expense expense, int displayedIndex) { + super(FXML); + + this.expense = expense; + id.setText(displayedIndex + ". "); + name.setText(expense.getItem().getName().name); + if (expense instanceof Allowance) { + cost.setText("Amount: $" + expense.getItem().getCost().toString()); + } else { + cost.setText("Cost: $" + expense.getItem().getCost().toString()); + } + SimpleDateFormat formatter = new SimpleDateFormat("EEE, MMM d, yyyy"); + date.setText(String.format("Added on: %s \n", formatter.format(expense.getDate()))); + initialiseTags(expense); + } + + /** + * Returns the color style for {@code tagName}'s label. + */ + private String getTagColorStyleFor(String tagName) { + // generate a random color from the hash code of the tag so the color remain consistent + // between different runs of the program while still making it random enough between tags. + return TAG_COLOR_STYLES[Math.abs(tagName.hashCode()) % TAG_COLOR_STYLES.length]; + } + + /** + * Creates the tag labels for {@code person}. + */ + private void initialiseTags(Expense expense) { + expense.getItem().getTags().forEach(tag -> { + Label tagLabel = new Label(tag.tagName); + tagLabel.getStyleClass().add(getTagColorStyleFor(tag.tagName)); + if (tag.tagName.equals("Expense") || tag.tagName.equals("Allowance")) { + typeTag.getChildren().add(tagLabel); + } else { + tags.getChildren().add(tagLabel); + } + }); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ExpenseCard)) { + return false; + } + + // state check + ExpenseCard card = (ExpenseCard) other; + return id.getText().equals(card.id.getText()) + && expense.equals(card.expense); + } +} diff --git a/src/main/java/seedu/address/ui/ExpenseListPanel.java b/src/main/java/seedu/address/ui/ExpenseListPanel.java new file mode 100644 index 000000000000..61015680083c --- /dev/null +++ b/src/main/java/seedu/address/ui/ExpenseListPanel.java @@ -0,0 +1,73 @@ +package seedu.address.ui; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.epiggy.Expense; + +/** + * Panel containing the list of expenses. + */ +public class ExpenseListPanel extends UiPart { + private static final String FXML = "ExpenseListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ExpenseListPanel.class); + + @FXML + + private ListView expenseListView; + + public ExpenseListPanel(ObservableList expenseList, ObservableValue selectedExpense, + Consumer onSelectedExpenseChange) { + super(FXML); + + expenseListView.setItems(expenseList); + expenseListView.setCellFactory(listView -> new ExpenseListViewCell()); + expenseListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + logger.fine("Selection in expense list panel changed to : '" + newValue + "'"); + onSelectedExpenseChange.accept(newValue); + }); + selectedExpense.addListener((observable, oldValue, newValue) -> { + logger.fine("Selected expense changed to: " + newValue); + + // Don't modify selection if we are already selecting the selected expense, + // otherwise we would have an infinite loop. + if (Objects.equals(expenseListView.getSelectionModel().getSelectedItem(), newValue)) { + return; + } + + if (newValue == null) { + expenseListView.getSelectionModel().clearSelection(); + } else { + int index = expenseListView.getItems().indexOf(newValue); + expenseListView.scrollTo(index); + expenseListView.getSelectionModel().clearAndSelect(index); + } + }); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Expense} using a {@code ExpenseCard}. + */ + class ExpenseListViewCell extends ListCell { + @Override + protected void updateItem(Expense expense, boolean empty) { + super.updateItem(expense, empty); + + if (empty || expense == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ExpenseCard(expense, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index ac165736001d..17383e5a9723 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -33,6 +33,9 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private BrowserPanel browserPanel; private PersonListPanel personListPanel; + private ExpenseListPanel expenseListPanel; + private SavingsPanel savingsPanel; + private BudgetPanel budgetPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @@ -46,7 +49,16 @@ public class MainWindow extends UiPart { private MenuItem helpMenuItem; @FXML - private StackPane personListPanelPlaceholder; + private MenuItem cleanText; + + @FXML + private StackPane savingsPanelPlaceholder; + + @FXML + private StackPane expenseListPanelPlaceholder; + + @FXML + private StackPane budgetPanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; @@ -75,10 +87,12 @@ public Stage getPrimaryStage() { private void setAccelerators() { setAccelerator(helpMenuItem, KeyCombination.valueOf("F1")); + setAccelerator(cleanText, KeyCombination.valueOf("F2")); } /** * Sets the accelerator of a MenuItem. + * * @param keyCombination the KeyCombination value of the accelerator */ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { @@ -114,14 +128,21 @@ void fillInnerParts() { browserPanel = new BrowserPanel(logic.selectedPersonProperty()); browserPlaceholder.getChildren().add(browserPanel.getRoot()); - personListPanel = new PersonListPanel(logic.getFilteredPersonList(), logic.selectedPersonProperty(), - logic::setSelectedPerson); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + expenseListPanel = new ExpenseListPanel(logic.getFilteredExpenseList(), + logic.selectedExpenseProperty(), expense -> { + }); + expenseListPanelPlaceholder.getChildren().add(expenseListPanel.getRoot()); + + savingsPanel = new SavingsPanel(logic.getFilteredExpenseList(), logic.getGoal(), logic::getSavings); + savingsPanelPlaceholder.getChildren().add(savingsPanel.getRoot()); + + budgetPanel = new BudgetPanel(logic.getFilteredBudgetList(), logic::setCurrentBudget); + budgetPanelPlaceholder.getChildren().add(budgetPanel.getRoot()); resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath(), logic.getAddressBook()); + StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getEPiggyFilePath(), logic.getEPiggy()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); CommandBox commandBox = new CommandBox(this::executeCommand, logic.getHistory()); @@ -134,6 +155,7 @@ void fillInnerParts() { private void setWindowDefaultSize(GuiSettings guiSettings) { primaryStage.setHeight(guiSettings.getWindowHeight()); primaryStage.setWidth(guiSettings.getWindowWidth()); + if (guiSettings.getWindowCoordinates() != null) { primaryStage.setX(guiSettings.getWindowCoordinates().getX()); primaryStage.setY(guiSettings.getWindowCoordinates().getY()); @@ -168,8 +190,32 @@ private void handleExit() { primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; + /** + * Shows completed summary to user. + */ + @FXML + private void handleReport() { + helpWindow.hide(); + try { + logic.execute("report"); + } catch (CommandException | ParseException e) { + resultDisplay.setFeedbackToUser(e.getMessage(), "report t/daily"); + } + } + + /** + * clean text area. + */ + @FXML + private void handleClean() { + helpWindow.hide(); + resultDisplay.clearDisplay(); + + } + + + public ExpenseListPanel getExpenseListPanel() { + return expenseListPanel; } /** @@ -181,7 +227,7 @@ private CommandResult executeCommand(String commandText) throws CommandException try { CommandResult commandResult = logic.execute(commandText); logger.info("Result: " + commandResult.getFeedbackToUser()); - resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser(), commandText); if (commandResult.isShowHelp()) { handleHelp(); @@ -194,7 +240,7 @@ private CommandResult executeCommand(String commandText) throws CommandException return commandResult; } catch (CommandException | ParseException e) { logger.info("Invalid command: " + commandText); - resultDisplay.setFeedbackToUser(e.getMessage()); + resultDisplay.setFeedbackToUser(e.getMessage(), commandText); throw e; } } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index f6727ea83abd..6ece59d4f40e 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -13,13 +13,15 @@ public class PersonCard extends UiPart { private static final String FXML = "PersonListCard.fxml"; + private static final String[] TAG_COLOR_STYLES = + { "turquoise", "orange", "yellow", "green", "black", "blue", "beige", "pink", "white", "grey" }; /** * 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 + * @see The issue on EPiggy level 4 */ public final Person person; @@ -47,7 +49,27 @@ public PersonCard(Person person, int displayedIndex) { phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); - person.getTags().forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + initialiseTags(person); + } + + /** + * Returns the color style for {@code tagName}'s label. + */ + private String getTagColorStyleFor(String tagName) { + // generate a random color from the hash code of the tag so the color remain consistent + // between different runs of the program while still making it random enough between tags. + return TAG_COLOR_STYLES[Math.abs(tagName.hashCode()) % TAG_COLOR_STYLES.length]; + } + + /** + * Creates the tag labels for {@code person}. + */ + private void initialiseTags(Person person) { + person.getTags().forEach(tag -> { + Label tagLabel = new Label(tag.tagName); + tagLabel.getStyleClass().add(getTagColorStyleFor(tag.tagName)); + tags.getChildren().add(tagLabel); + }); } @Override diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index 5ca3fa4fc671..4c7e0eb942da 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -24,7 +24,7 @@ public class PersonListPanel extends UiPart { private ListView personListView; public PersonListPanel(ObservableList personList, ObservableValue selectedPerson, - Consumer onSelectedPersonChange) { + Consumer onSelectedPersonChange) { super(FXML); personListView.setItems(personList); personListView.setCellFactory(listView -> new PersonListViewCell()); diff --git a/src/main/java/seedu/address/ui/ReportData.java b/src/main/java/seedu/address/ui/ReportData.java new file mode 100644 index 000000000000..1c61a94a6d98 --- /dev/null +++ b/src/main/java/seedu/address/ui/ReportData.java @@ -0,0 +1,45 @@ +package seedu.address.ui; + +/** + * Data class is used for collecting data from expenses, savings and budgets. + * Data class is also used for spot the data to the report chart. + */ +public class ReportData { + private int year; + private double budget; + private double expense; + private double allowance; + + + public ReportData(int year) { + this.year = year; + } + + public double getAllowance() { + return allowance; + } + + public void setAllowance(double allowance) { + this.allowance = allowance; + } + + public double getBudget() { + return budget; + } + + public void setBudget(double budget) { + this.budget = budget; + } + + public double getExpense() { + return expense; + } + + public void setExpense(double expense) { + this.expense = expense; + } + + public double updateValue(double original, double newValue) { + return original + newValue; + } +} diff --git a/src/main/java/seedu/address/ui/ReportWindow.java b/src/main/java/seedu/address/ui/ReportWindow.java new file mode 100644 index 000000000000..d385f1825e08 --- /dev/null +++ b/src/main/java/seedu/address/ui/ReportWindow.java @@ -0,0 +1,640 @@ +package seedu.address.ui; + +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.logging.Logger; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.chart.AreaChart; +import javafx.scene.chart.BarChart; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.PieChart; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.Model; +import seedu.address.model.epiggy.Allowance; +import seedu.address.model.epiggy.Budget; +import seedu.address.model.epiggy.Expense; + +/** + * Report Window. Provides report and chart to the user. + */ +//@@author yunjun199321 +public class ReportWindow { + private final Logger logger = LogsCenter.getLogger(getClass()); + private Stage window; + + /** + * A method controls report chart display. + * + * @param model EPiggy model + * @param date User specified date, month or year + * @param type Report display type + */ + public void displayReportController(Model model, LocalDate date, String type) { + logger.info("Creates Report window"); + ReportDisplayType expenseDisplayType = ReportDisplayType.valueOf(type); + window = new Stage(); + window.initModality(Modality.APPLICATION_MODAL); + window.setTitle("Report"); + switch (expenseDisplayType) { + + case DAY: + displayReportOnSpecifiedDay(model, date); + break; + case MONTH: + displayReportOnSpecifiedMonth(model, date); + break; + case YEAR: + displayReportOnSpecifiedYear(model, date); + break; + case ALL: + default: + displayCompleteReport(model); + break; + } + window.showAndWait(); + + } + + /** + * Display daily summary on area chart. + */ + private void displayReportOnSpecifiedDay(Model model, LocalDate date) { + double minSpend = Double.MAX_VALUE; // minimum spend of the day + double maxSpend = Double.MIN_VALUE; // maximum spend of the day + double totalSpend = 0; // total spend of the day + boolean isEmptyExpenseData = true; + + // Creates an Area Chart + final NumberAxis xAxis = new NumberAxis(0, 24, 1); + final NumberAxis yAxis = new NumberAxis(); + final AreaChart areaChart = + new AreaChart<>(xAxis, yAxis); + areaChart.setTitle("Report for date: " + date); + + XYChart.Series seriesExpense = new XYChart.Series(); + seriesExpense.setName("Expense"); + yAxis.setLabel("Expense"); + xAxis.setLabel("Hours"); + Calendar calExpenseDay = Calendar.getInstance(); + Calendar calSpecifiedDay = Calendar.getInstance(); + calSpecifiedDay.setTime(Date.from(date.atStartOfDay().atZone(ZoneId.systemDefault()) + .toInstant())); // coverts localDate to calendar. + + final ObservableList expenses = model.getFilteredExpenseList(); + double[] hours = new double[24]; + if (!expenses.isEmpty()) { + // expense is not empty + for (Expense expense : expenses) { + Date currentDate = expense.getDate(); + // convert to calender object. + calExpenseDay.setTime(currentDate); + // find specified date and type of the expense is not allowance + if (calExpenseDay.get(Calendar.DAY_OF_MONTH) + == calSpecifiedDay.get(Calendar.DAY_OF_MONTH) + && !expense.getItem().getName() + .toString().equals("Allowance")) { + // data is not empty + isEmptyExpenseData = false; + // find min value + double price = expense.getItem().getCost().getAmount(); + if (price < minSpend) { + minSpend = price; + } + // find max value + if (price > maxSpend) { + maxSpend = price; + } + // calculate total value + totalSpend += price; + // hour as index, amount as value + int hour = calExpenseDay.get(Calendar.HOUR_OF_DAY); + hours[hour] += expense.getItem().getCost().getAmount(); + } + } + for (int i = 0; i < hours.length; i++) { + seriesExpense.getData().add(new XYChart.Data(i, hours[i])); // spot data to the chart + } + } + // JavaFx chart setup + // create a layout of the new window + VBox layout = new VBox(10); + Label min = new Label(); + Label max = new Label(); + Label total = new Label(); + + if (!isEmptyExpenseData) { + min.setText("The minimum amount of expense for today: S$" + minSpend); + max.setText("The maximum amount of expense for today: S$" + maxSpend); + total.setText("The total amount of expense for today: S$" + totalSpend); + layout.getChildren().addAll(areaChart, min, max, total); + // JavaFx bug, need to manually set all nodes margin!!! + VBox.setMargin(areaChart, new Insets(10, 20, 10, 10)); + VBox.setMargin(min, new Insets(5, 10, 0, 50)); + VBox.setMargin(max, new Insets(5, 10, 0, 50)); + VBox.setMargin(total, new Insets(5, 10, 0, 50)); + } else { + total.setText("No Record found!"); + layout.setAlignment(Pos.CENTER); + layout.getChildren().addAll(areaChart, total); + } + Scene scene = new Scene(layout, 800, 600); + areaChart.getData().add(seriesExpense); + window.setScene(scene); + } + + /** + * Displays monthly summary on line chart. + */ + private void displayReportOnSpecifiedMonth(Model model, LocalDate date) { + Calendar cal = Calendar.getInstance(); + Date targetDate = Date.from(date.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + cal.setTime(targetDate); + + //defining the axes + final NumberAxis xAxis = new NumberAxis(1, 31, 1); + final NumberAxis yAxis = new NumberAxis(); + xAxis.setLabel("Days"); + yAxis.setLabel("Amount"); + + //creates the chart + final LineChart lineChart = new LineChart<>(xAxis, yAxis); + lineChart.setTitle("Report for month: " + date.getMonth().toString() + .substring(0, 1).toUpperCase() + date.getMonth().toString().substring(1).toLowerCase() + + " " + date.getYear()); // Format chart title. + XYChart.Series seriesExpense = new XYChart.Series(); + seriesExpense.setName("Expense"); + XYChart.Series seriesAllowance = new XYChart.Series(); + seriesAllowance.setName("Allowance"); + + final ObservableList expenses = model.getFilteredExpenseList(); + double[] exps = new double[31]; + double[] allowances = new double[31]; + double totalAllowance = 0; // total allowance of the month + double totalExpense = 0; // total expense of the month + double minExpense = Double.MAX_VALUE; // a minimum amount of expense within the month + double maxExpense = Double.MIN_VALUE; // a maximum amount of expense within the month + int dayWithMinExpense = 0; // the day with minimum expense + int dayWithMaxExpense = 0; // the day with maximum expense + boolean isExpenseDataEmpty = true; + boolean isAllowanceDataEmpty = true; + + if (!expenses.isEmpty()) { + for (Expense expense : expenses) { + LocalDate currentDay = expense.getDate().toInstant().atZone(ZoneId.systemDefault()) + .toLocalDate(); + // find same year and same month with the target month + if (currentDay.getMonthValue() == date.getMonthValue() + && currentDay.getYear() == date.getYear()) { + // -1 because getMonthValue from 1 to 12 + if (expense instanceof Allowance) { + // calculate total allowance + allowances[currentDay.getMonthValue() - 1] += expense.getItem().getCost().getAmount(); + isAllowanceDataEmpty = false; + } else { + exps[currentDay.getMonthValue() - 1] += expense.getItem().getCost().getAmount(); + isExpenseDataEmpty = false; + } + } + } + } + // spot the data into the chart + for (int i = 0; i < allowances.length; i++) { + // calculate total expense and allowance + totalAllowance += allowances[i]; + totalExpense += exps[i]; + // get the days with min, max expense value + if (exps[i] < minExpense) { + minExpense = exps[i]; + dayWithMinExpense = i + 1; // day is not start from 0. + } + if (exps[i] > maxExpense) { + maxExpense = exps[i]; + dayWithMaxExpense = i + 1; + } + // spot the chart + seriesExpense.getData().add(new XYChart.Data(i + 1, exps[i])); + seriesAllowance.getData().add(new XYChart.Data(i + 1, allowances[i])); + } + // JavaFX stage content setup + lineChart.getData().addAll(seriesExpense, seriesAllowance); + VBox layout = new VBox(10); + Label labelOfTotalExpense = new Label(); + Label labelOfTotalAllowance = new Label(); + Label labelOfTotalSaving = new Label(); + Label labelOfMinExpenseDay = new Label(); + Label labelOfMaxExpenseDay = new Label(); + + if (!isAllowanceDataEmpty || !isExpenseDataEmpty) { + if (isExpenseDataEmpty) { + minExpense = 0; + maxExpense = 0; + } + // adds labels into layout + labelOfTotalExpense.setText("The total amount of expense on this month: S$" + + totalExpense); + labelOfTotalAllowance.setText("The total amount of allowance on this month: S$" + + totalAllowance); + labelOfTotalSaving.setText("The total amount of Saving on this month: S$" + + String.format("%.2f", totalAllowance - totalExpense)); + labelOfMinExpenseDay.setText("The lowest expense record is S$" + + minExpense + + " at " + + dayWithMinExpense + + " " + + new SimpleDateFormat("MMM").format(cal.getTime()) + + " " + + cal.get(Calendar.YEAR)); + labelOfMaxExpenseDay.setText("The highest expense record is S$" + + maxExpense + + " at " + + dayWithMaxExpense + + " " + + new SimpleDateFormat("MMM").format(cal.getTime()) + + " " + + cal.get(Calendar.YEAR)); + layout.getChildren().addAll(lineChart, labelOfTotalExpense, labelOfTotalAllowance, + labelOfTotalSaving, labelOfMaxExpenseDay, labelOfMinExpenseDay); + // JavaFx bug, need to manually set all nodes margin!!! + VBox.setMargin(lineChart, new Insets(10, 20, 10, 10)); + VBox.setMargin(labelOfTotalAllowance, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfTotalExpense, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfTotalSaving, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfMaxExpenseDay, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfMinExpenseDay, new Insets(5, 10, 0, 50)); + } else { + layout.getChildren().addAll(lineChart, labelOfTotalExpense); + labelOfTotalExpense.setText("No record found!"); + layout.setAlignment(Pos.CENTER); + // JavaFx bug, need to manually set all nodes margin!!! + VBox.setMargin(lineChart, new Insets(10, 20, 10, 10)); + VBox.setMargin(labelOfTotalExpense, new Insets(5, 10, 0, 50)); + } + Scene scene = new Scene(layout, 800, 600); + window.setScene(scene); + } + + /** + * Display the proportion of income spent on different categories on pie chart. + */ + private Group displayExpensePercentageReport(double totalExpense, double totalAllowance) { + + double totalSaving = (totalAllowance < totalExpense) ? 0 : (totalAllowance - totalExpense); + ObservableList pieChartData = + FXCollections.observableArrayList( + new PieChart.Data("Total expense", totalExpense), + new PieChart.Data("Total saving", totalSaving)); + final PieChart chart = new PieChart(pieChartData); + chart.setTitle("Percentage of total saving over total expense"); + //setting the direction to arrange the data + chart.setClockwise(true); + //Setting legend and labels + chart.setLabelLineLength(15); + //Setting the labels of the pie chart visible + chart.setLabelsVisible(true); + //Setting the start angle of the pie chart + chart.setStartAngle(180); + //Creating a Group object + return new Group(chart); + } + + /** + * Displays yearly summary on bar chart. + */ + private void displayReportOnSpecifiedYear(Model model, LocalDate date) { + final NumberAxis yAxis = new NumberAxis(); + final CategoryAxis xAxis = new CategoryAxis(); + final BarChart bc = + new BarChart<>(xAxis, yAxis); + bc.setTitle("Report for year: " + date.getYear()); + yAxis.setLabel("Amount"); + xAxis.setLabel("Months"); + + XYChart.Series seriesAllowance = new XYChart.Series(); + seriesAllowance.setName("Allowance"); + XYChart.Series seriesExpense = new XYChart.Series(); + seriesExpense.setName("Expense"); + XYChart.Series seriesBudget = new XYChart.Series(); + seriesBudget.setName("Budget"); + + final ObservableList budgetList = model.getFilteredBudgetList(); + final ObservableList expenseList = model.getFilteredExpenseList(); + + double[] budgets = new double[12]; + double[] allowances = new double[12]; + double[] expenses = new double[12]; + double totalAllowance = 0; // total allowance of the year + double totalExpense = 0; // total expense of the year + double totalBudget = 0; // total budget of the year + double minExpenseValue = Double.MAX_VALUE; + double maxExpenseValue = Double.MIN_VALUE; + int monthWithMinExpense = 0; // the day with minimum expense + int monthWithMaxExpense = 0; // the day with maximum expense + boolean isExpenseEmpty = true; + boolean isBudgetEmpty = true; + boolean isAllowanceEmpty = true; + + if (!expenseList.isEmpty()) { + for (Expense expense : expenseList) { + LocalDate currentDate = expense.getDate().toInstant().atZone(ZoneId.systemDefault()) + .toLocalDate(); + if (currentDate.getYear() == date.getYear()) { + // found the specified year + double value = expense.getItem().getCost().getAmount(); + if (expense instanceof Allowance) { + // allowance + allowances[currentDate.getMonthValue() - 1] += value; + isAllowanceEmpty = false; + } else { + // expense + expenses[currentDate.getMonthValue() - 1] += value; + isExpenseEmpty = false; + } + } + } + } + if (!budgetList.isEmpty()) { + for (Budget budget : budgetList) { + LocalDate currentDate = budget.getStartDate().toInstant().atZone(ZoneId.systemDefault()) + .toLocalDate(); + if (currentDate.getYear() == date.getYear()) { + budgets[currentDate.getMonthValue() - 1] += budget.getBudgetedAmount().getAmount(); + isBudgetEmpty = false; + } + } + } + + String[] months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", + "Sep", "Oct", "Nov", "Dec"}; + + String[] monthsLong = {"January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December"}; + for (int i = 0; i < expenses.length; i++) { + // calculate total expense and allowance + totalExpense += expenses[i]; + totalAllowance += allowances[i]; + totalBudget += budgets[i]; + + // get the days with min, max expense value + if (expenses[i] < minExpenseValue) { + minExpenseValue = expenses[i]; + monthWithMinExpense = i; + } + if (expenses[i] > maxExpenseValue) { + maxExpenseValue = expenses[i]; + monthWithMaxExpense = i; + } + seriesExpense.getData().add(new XYChart.Data(months[i], expenses[i])); + seriesBudget.getData().add(new XYChart.Data(months[i], budgets[i])); + seriesAllowance.getData().add(new XYChart.Data(months[i], allowances[i])); + } + + VBox layout = new VBox(10); + Label labelOfTotalExpense = new Label(); + Label labelOfTotalAllowance = new Label(); + Label labelOfTotalSaving = new Label(); + Label labelOfTotalBudget = new Label(); + Label labelOfMinExpenseDay = new Label(); + Label labelOfMaxExpenseDay = new Label(); + + if (!isAllowanceEmpty || !isBudgetEmpty || !isExpenseEmpty) { + if (isExpenseEmpty) { + minExpenseValue = 0; + maxExpenseValue = 0; + } + // adds labels into layout + labelOfTotalExpense.setText("The total amount of expense on this year: S$" + + totalExpense); + labelOfTotalAllowance.setText("The total amount of allowance on this year: S$" + + totalAllowance); + labelOfTotalSaving.setText("The total amount of saving on this year: S$" + + String.format ("%.2f", totalAllowance - totalExpense)); + labelOfTotalBudget.setText("The total amount of budget on this year: S$" + + totalBudget); + labelOfMinExpenseDay.setText(monthsLong[monthWithMinExpense] + + " is the least consumed month in " + + date.getYear() + + ". The lowest expense record is S$" + + minExpenseValue); + + labelOfMaxExpenseDay.setText(monthsLong[monthWithMaxExpense] + + " is the most consumed month in " + + date.getYear() + + ". The highest expense record is S$" + + maxExpenseValue); + layout.getChildren().addAll(bc, labelOfTotalExpense, labelOfTotalAllowance, + labelOfTotalBudget, labelOfTotalSaving, labelOfMaxExpenseDay, labelOfMinExpenseDay); + VBox.setMargin(bc, new Insets(10, 20, 10, 10)); + VBox.setMargin(labelOfTotalAllowance, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfTotalExpense, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfTotalSaving, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfTotalBudget, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfMaxExpenseDay, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfMinExpenseDay, new Insets(5, 10, 0, 50)); + } else { + labelOfTotalExpense.setText("No record found!"); + layout.getChildren().addAll(bc, labelOfTotalExpense); + VBox.setMargin(bc, new Insets(10, 20, 10, 10)); + layout.setAlignment(Pos.CENTER); + } + Scene scene = new Scene(layout, 800, 650); + bc.getData().addAll(seriesBudget, seriesExpense, seriesAllowance); + window.setScene(scene); + + } + + /** + * Displays all expenses, allowances and budgets of the user on bar chart. + */ + private void displayCompleteReport(Model model) { + + final NumberAxis yAxis = new NumberAxis(); + final CategoryAxis xAxis = new CategoryAxis(); + final BarChart bc = + new BarChart<>(xAxis, yAxis); + + final ObservableList budgets = model.getFilteredBudgetList(); + final ObservableList expenses = model.getFilteredExpenseList(); + + boolean isExpenseEmpty = true; + + bc.setTitle("Completed Summary"); + yAxis.setLabel("Amount"); + xAxis.setLabel("Year"); + + HashMap map = new HashMap<>(); + // convert expense data into ReportData + if (!expenses.isEmpty()) { + for (int i = 0; i < expenses.size(); i++) { + int year = expenses.get(i).getDate().toInstant().atZone(ZoneId.systemDefault()) + .toLocalDate().getYear(); // get year from expense + ReportData data; + + double amount = expenses.get(i).getItem().getCost().getAmount(); + if (map.containsKey(year)) { + // if year data exists + ReportData temp = map.get(year); + if (expenses.get(i) instanceof Allowance) { + // expense type is allowance. + temp.setAllowance(temp.updateValue(temp.getAllowance(), amount)); + } else { + // expense type is expense. + temp.setExpense(temp.updateValue(temp.getExpense(), amount)); + isExpenseEmpty = false; + } + map.put(year, temp); + } else { + // year data does not exist + data = new ReportData(year); + if (expenses.get(i) instanceof Allowance) { + data.setAllowance(amount); + } else { + data.setExpense(amount); + isExpenseEmpty = false; + } + map.put(year, data); + } + } + } + // convert expense data to ReportData + if (!budgets.isEmpty()) { + for (int i = 0; i < budgets.size(); i++) { + int year = budgets.get(i).getStartDate().toInstant().atZone(ZoneId.systemDefault()) + .toLocalDate().getYear(); // get year from expense + ReportData data; + double amount = budgets.get(i).getBudgetedAmount().getAmount(); + + if (map.containsKey(year)) { + // if year data exists + ReportData temp = map.get(year); + temp.setBudget(temp.updateValue(temp.getBudget(), amount)); + map.put(year, temp); + } else { + // year data does not exist + data = new ReportData(year); + data.setBudget(amount); + map.put(year, data); + } + } + } + XYChart.Series series1 = new XYChart.Series(); + series1.setName("Allowance"); + + XYChart.Series series2 = new XYChart.Series(); + series2.setName("Expense"); + + XYChart.Series series3 = new XYChart.Series(); + series3.setName("Budget"); + + double totalExpense = 0; + double totalBudget = 0; + double totalAllowance = 0; + double maxExpense = 0; + String yearWithMaxExpense = ""; + TreeMap tm = new TreeMap<>(map); + for (Map.Entry entry : tm.entrySet()) { + totalAllowance += entry.getValue().getAllowance(); + totalBudget += entry.getValue().getBudget(); + totalExpense += entry.getValue().getExpense(); + + if (entry.getValue().getExpense() > maxExpense) { + maxExpense = entry.getValue().getExpense(); + yearWithMaxExpense = entry.getKey().toString(); + } + + series1.getData().add(new XYChart.Data(entry.getKey().toString(), + entry.getValue().getAllowance())); + series2.getData().add(new XYChart.Data(entry.getKey().toString(), + entry.getValue().getExpense())); + series3.getData().add(new XYChart.Data(entry.getKey().toString(), + entry.getValue().getBudget())); + } + VBox layout = new VBox(10); + Label labelOfTotalExpense = new Label(); + Label labelOfTotalAllowance = new Label(); + Label labelOfTotalSaving = new Label(); + Label labelOfTotalBudget = new Label(); + Label labelOfMaxExpenseValue = new Label(); + Label labelOfMaxExpenseYear = new Label(); + if (!tm.isEmpty()) { + + // adds labels into layout + labelOfTotalExpense.setText("The total amount of expense: S$" + + totalExpense); + labelOfTotalAllowance.setText("The total amount of allowance: S$" + + totalAllowance); + labelOfTotalSaving.setText("The total amount of saving: S$" + + String.format ("%.2f", totalAllowance - totalExpense)); + labelOfTotalBudget.setText("The total amount of budget: S$" + + totalBudget); + labelOfMaxExpenseYear.setText(yearWithMaxExpense + + " is the most consumed year. " + + "The highest expense record is S$" + + maxExpense); + + if (isExpenseEmpty) { + labelOfMaxExpenseYear.setText(""); + } + + if (!isExpenseEmpty) { + // show pie chart + Group pieChart = displayExpensePercentageReport(totalExpense, totalAllowance); + + layout.getChildren().addAll(bc, pieChart, labelOfTotalExpense, labelOfTotalBudget, + labelOfTotalAllowance, labelOfTotalSaving, labelOfMaxExpenseYear, labelOfMaxExpenseValue); + } else { + layout.getChildren().addAll(bc, labelOfTotalExpense, labelOfTotalBudget, + labelOfTotalAllowance, labelOfTotalSaving, labelOfMaxExpenseYear, labelOfMaxExpenseValue); + } + + // creates a scroll pane + ScrollPane sp = new ScrollPane(); + sp.setContent(layout); + // JavaFx bug, need to manually set all nodes margin!!! + VBox.setMargin(bc, new Insets(10, 20, 10, 10)); + VBox.setMargin(labelOfTotalAllowance, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfTotalExpense, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfTotalSaving, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfTotalBudget, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfMaxExpenseValue, new Insets(5, 10, 0, 50)); + VBox.setMargin(labelOfMaxExpenseYear, new Insets(5, 10, 0, 50)); + Scene scene = new Scene(sp, 800, 650); + bc.getData().addAll(series1, series2, series3); + window.setScene(scene); + } else { + labelOfTotalExpense.setText("No record found!"); + layout.getChildren().addAll(bc, labelOfTotalExpense); + layout.setAlignment(Pos.CENTER); + Scene scene = new Scene(layout, 800, 650); + bc.getData().addAll(series1, series2, series3); + window.setScene(scene); + } + + } + + /** + * Chart will be displayed according to report display type. + */ + private enum ReportDisplayType { + MONTH, DAY, YEAR, ALL + } +} diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/address/ui/ResultDisplay.java index 7d98e84eedf0..f714ea6c2c16 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/address/ui/ResultDisplay.java @@ -6,6 +6,7 @@ import javafx.scene.control.TextArea; import javafx.scene.layout.Region; + /** * A ui for the status bar that is displayed at the header of the application. */ @@ -13,16 +14,30 @@ public class ResultDisplay extends UiPart { private static final String FXML = "ResultDisplay.fxml"; + private String messages = ""; + @FXML private TextArea resultDisplay; public ResultDisplay() { super(FXML); + resultDisplay.setText("Welcome to ePiggy! " + + "Enter a command to get started, or enter 'help' to view all the available commands!"); } - public void setFeedbackToUser(String feedbackToUser) { + public void setFeedbackToUser(String feedbackToUser, String commandEntered) { requireNonNull(feedbackToUser); - resultDisplay.setText(feedbackToUser); + + messages = "========================\n" + "ePiggy: " + feedbackToUser + "\n\n" + "You: " + commandEntered + + "\n" + messages; + resultDisplay.setText(messages); } + /** + * Clear all text in the textBox. + */ + public void clearDisplay() { + messages = ""; + resultDisplay.clear(); + } } diff --git a/src/main/java/seedu/address/ui/SavingsPanel.java b/src/main/java/seedu/address/ui/SavingsPanel.java new file mode 100644 index 000000000000..a7bc42092238 --- /dev/null +++ b/src/main/java/seedu/address/ui/SavingsPanel.java @@ -0,0 +1,80 @@ +package seedu.address.ui; + +import java.util.function.Supplier; +import java.util.logging.Logger; + +import javafx.beans.value.ObservableValue; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.epiggy.Expense; +import seedu.address.model.epiggy.Goal; +import seedu.address.model.epiggy.item.Cost; + +//@@author kev-inc + +/** + * Panel containing savings information. + */ +public class SavingsPanel extends UiPart { + + private static final String FXML = "SavingsPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(SavingsPanel.class); + + @FXML + private Label currentSavings; + + @FXML + private Label currentGoal; + + @FXML + private Label amountDifferenceTitle; + + @FXML + private Label amountDifference; + + public SavingsPanel(ObservableList expense, ObservableValue goal, + Supplier> onSavingsChange) { + super(FXML); + + expense.addListener((ListChangeListener) observable -> { + refreshPanel(goal, onSavingsChange); + }); + + goal.addListener(observable -> { + refreshPanel(goal, onSavingsChange); + }); + + refreshPanel(goal, onSavingsChange); + } + + /** + * Updates the savings panel with new changes. + * @param goal + * @param onSavingsChange + */ + private void refreshPanel(ObservableValue goal, Supplier> onSavingsChange) { + currentSavings.setText("$" + onSavingsChange.get().getValue().toString()); + if (goal.getValue() == null) { + currentGoal.setText("(None set)"); + amountDifference.setText("$0.00"); + } else { + currentGoal.setText(goal.getValue().toString()); + + Cost goalAmount = goal.getValue().getAmount(); + Cost savingsAmount = onSavingsChange.get().getValue(); + Cost diff = new Cost(goalAmount.getAmount() - savingsAmount.getAmount()); + + if (diff.getAmount() > 0) { + amountDifferenceTitle.setVisible(true); + amountDifference.setText("$" + diff); + } else { + amountDifferenceTitle.setVisible(false); + amountDifference.setText("Congratulations!\nYou've reached your\nsavings goal!"); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java index b22e1f525256..871b930f959a 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/StatusBarFooter.java @@ -8,7 +8,8 @@ import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.layout.Region; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyEPiggy; + /** * A ui for the status bar that is displayed at the footer of the application. @@ -36,9 +37,9 @@ public class StatusBarFooter extends UiPart { private Label saveLocationStatus; - public StatusBarFooter(Path saveLocation, ReadOnlyAddressBook addressBook) { + public StatusBarFooter(Path saveLocation, ReadOnlyEPiggy ePiggy) { super(FXML); - addressBook.addListener(observable -> updateSyncStatus()); + ePiggy.addListener(observable -> updateSyncStatus()); syncStatus.setText(SYNC_STATUS_INITIAL); saveLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); } diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png index 29810cf1fd93..ec594871c546 100644 Binary files a/src/main/resources/images/address_book_32.png and b/src/main/resources/images/address_book_32.png differ diff --git a/src/main/resources/images/address_book_32_old.png b/src/main/resources/images/address_book_32_old.png new file mode 100644 index 000000000000..29810cf1fd93 Binary files /dev/null and b/src/main/resources/images/address_book_32_old.png differ diff --git a/src/main/resources/images/info_icon.png b/src/main/resources/images/info_icon.png index f8cef714095b..bebda9290d36 100644 Binary files a/src/main/resources/images/info_icon.png and b/src/main/resources/images/info_icon.png differ diff --git a/src/main/resources/view/BudgetListCard.fxml b/src/main/resources/view/BudgetListCard.fxml new file mode 100644 index 000000000000..9a4395888d93 --- /dev/null +++ b/src/main/resources/view/BudgetListCard.fxml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/BudgetPanel.fxml b/src/main/resources/view/BudgetPanel.fxml new file mode 100644 index 000000000000..36ac379cea4e --- /dev/null +++ b/src/main/resources/view/BudgetPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8d..9f3ce68d90f6 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -17,6 +17,13 @@ -fx-opacity: 1; } +.savings-text { + -fx-font-size: 18pt; + -fx-font-family: "Segoe UI Semibold"; + -fx-text-fill: white; + -fx-opacity: 1; +} + .label-header { -fx-font-size: 32pt; -fx-font-family: "Segoe UI Light"; @@ -337,16 +344,126 @@ -fx-background-radius: 0; } +#typeTag { + -fx-hgap: 7; + -fx-vgap: 3; +} + +#typeTag .label { + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +#typeTag .turquoise { + -fx-text-fill: black; + -fx-background-color: #40E0D0; +} + +#typeTag .orange { + -fx-text-fill: black; + -fx-background-color: orange; +} + +#typeTag .yellow { + -fx-background-color: yellow; + -fx-text-fill: black; +} + +#typeTag .green { + -fx-text-fill: black; + -fx-background-color: green; +} + +#typeTag .black { + -fx-text-fill: white; + -fx-background-color: black; +} + +#typeTag .blue { + -fx-text-fill: white; + -fx-background-color: blue; +} + +#typeTag .beige { + -fx-text-fill: white; + -fx-background-color: #F5F5DC; +} + +#typeTag .pink { + -fx-text-fill: black; + -fx-background-color: pink; +} + +#typeTag .white { + -fx-text-fill: black; + -fx-background-color: white; +} + +#typeTag .grey { + -fx-text-fill: white; + -fx-background-color: grey; +} + #tags { -fx-hgap: 7; -fx-vgap: 3; } #tags .label { - -fx-text-fill: white; - -fx-background-color: #3e7b91; -fx-padding: 1 3 1 3; -fx-border-radius: 2; -fx-background-radius: 2; -fx-font-size: 11; } + +#tags .turquoise { + -fx-text-fill: black; + -fx-background-color: #40E0D0; +} + +#tags .orange { + -fx-text-fill: black; + -fx-background-color: orange; +} + +#tags .yellow { + -fx-background-color: yellow; + -fx-text-fill: black; +} + +#tags .green { + -fx-text-fill: black; + -fx-background-color: green; +} + +#tags .black { + -fx-text-fill: white; + -fx-background-color: black; +} + +#tags .blue { + -fx-text-fill: white; + -fx-background-color: blue; +} + +#tags .beige { + -fx-text-fill: white; + -fx-background-color: #F5F5DC; +} + +#tags .pink { + -fx-text-fill: black; + -fx-background-color: pink; +} + +#tags .white { + -fx-text-fill: black; + -fx-background-color: white; +} + +#tags .grey { + -fx-text-fill: white; + -fx-background-color: grey; +} diff --git a/src/main/resources/view/ExpenseListCard.fxml b/src/main/resources/view/ExpenseListCard.fxml new file mode 100644 index 000000000000..e22114bcb264 --- /dev/null +++ b/src/main/resources/view/ExpenseListCard.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ExpenseListPanel.fxml b/src/main/resources/view/ExpenseListPanel.fxml new file mode 100644 index 000000000000..a92ed5f4f164 --- /dev/null +++ b/src/main/resources/view/ExpenseListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index c07e8e685014..c32fd06ba10f 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -4,9 +4,8 @@ - + title="Help" maximized="false"> diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 601ddabf2b5e..80f8477ade01 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -10,59 +10,85 @@ + - - - - - - - - - + title="ePiggy" minWidth="1400" minHeight="600" onCloseRequest="#handleExit" > - - - - - - - - - + + + + + + + + + - - - - - + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - + + + + + + - - - - + + + + + + + + + + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 58d5ad3dc56c..de3416a56889 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -3,7 +3,6 @@ - -